PageSourceSearch

https://rosgan.app/lacassina

html rosgan.app collected 2026-10-09 23:24:00 UTC 2,958,199 bytes, 34,082 lines download raw bytes

1<!DOCTYPE html>
2<html>
3<head>
4<meta charset="utf-8">
5<meta name="rosgan-release" content="rosganapp-build-2026-10-09-019">
6<link rel="stylesheet" href="/assets/app-update.css">
7<script src="/assets/app-update.js" defer></script>
7
8<meta name="viewport" content="width=device-width, initial-scale=1">
9<meta name="apple-mobile-web-app-capable" content="yes">
10<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
11<title>Rosgan</title>
12<base href="/">
13<script src="/assets/rosgan-brand-bug.js" defer></script>
vendor: 1 bytes, line 13
13
14<script>if (/^\/remates\/[^/]+\/tv\/?$/.test(location.pathname)) document.documentElement.setAttribute('data-rosgan-tv-output','true');</script>
14
15<link rel="icon" type="image/png" href="/favicon.png">
16<link rel="apple-touch-icon" href="/favicon.png">
17<meta property="og:site_name" content="Rosgan" data-rosgan-social>
18<meta property="og:type" content="website" data-rosgan-social>
19<meta property="og:title" content="Rosgan" data-rosgan-social>
20<meta property="og:description" content="Catálogo Rosgan" data-rosgan-social>
21<meta property="og:image" content="/favicon.png" data-rosgan-social>
22<meta property="og:image:secure_url" content="/favicon.png" data-rosgan-social>
23<meta name="twitter:card" content="summary_large_image" data-rosgan-social>
24<meta name="twitter:title" content="Rosgan" data-rosgan-social>
25<meta name="twitter:description" content="Catálogo Rosgan" data-rosgan-social>
26<meta name="twitter:image" content="/favicon.png" data-rosgan-social>
27<script>
28(function () {
29  var enabled = false;
30  if (!enabled) return;
31
32  document.documentElement.setAttribute('data-safari-debug', 'true');
33  var entries = [];
34  var startedAt = Date.now();
35  var panel = null;
36  var output = null;
37  var collapsed = false;
38
39  function safePath(value) {
40    var text = String(value || '');
41    try {
42      var anchor = document.createElement('a');
43      anchor.href = text;
44      return (anchor.pathname || '/') + (anchor.hash ? '#…' : '');
45    } catch (error) {
46      return text.split('?')[0].split('#')[0];
47    }
48  }
49
50  function redact(value) {
51    var text = String(value == null ? '' : value);
52    text = text.replace(/Bearer\s+[^\s]+/gi, 'Bearer [oculto]');
53    text = text.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email oculto]');
54    text = text.replace(/https?:\/\/[^\s)]+/gi, function (url) { return safePath(url); });
55    text = text.replace(/eyJ[A-Za-z0-9._-]{20,}/g, '[token oculto]');
56    return text.slice(0, 320);
57  }
58
59  function add(kind, message) {
60    entries.push({
61      at: ((Date.now() - startedAt) / 1000).toFixed(1) + 's',
62      kind: kind,
63      message: redact(message)
64    });
65    if (entries.length > 24) entries.shift();
66    render();
67  }
68
69  function isVisible(element) {
70    if (!element || !window.getComputedStyle) return false;
71    var style = window.getComputedStyle(element);
72    var rect = element.getBoundingClientRect();
73    return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0.01 && rect.width > 0 && rect.height > 0;
74  }
75
76  function resourceSummary() {
77    var wanted = ['react.production.min.js', 'react-dom.production.min.js', 'support.js', 'firebase-app-compat.js', 'firebase-auth-compat.js'];
78    var resources = [];
79    try {
80      resources = window.performance && window.performance.getEntriesByType ? window.performance.getEntriesByType('resource') : [];
81    } catch (error) {}
82    return wanted.map(function (name) {
83      var tag = Array.prototype.slice.call(document.scripts || []).filter(function (script) {
84        return String(script.src || '').indexOf(name) !== -1;
85      })[0];
86      var timing = resources.filter(function (item) {
87        return String(item.name || '').indexOf(name) !== -1;
88      })[0];
89      var duration = timing ? Math.round(Number(timing.duration || 0)) + 'ms' : 'sin timing';
90      return '  ' + name.replace('.production.min.js', '').replace('.js', '') + ': ' + (tag ? 'tag OK' : 'SIN TAG') + ' · ' + duration;
91    }).join('\n');
92  }
93
94  function diagnosticText() {
95    var root = document.documentElement;
96    var fallback = document.getElementById('rosgan-static-boot');
97    var authCards = document.querySelectorAll ? document.querySelectorAll('.rosgan-auth-card') : [];
98    var visibleAuthCard = false;
99    var authCardDetails = 'no encontrada';
100    for (var i = 0; i < authCards.length; i += 1) {
101      var authStyle = window.getComputedStyle ? window.getComputedStyle(authCards[i]) : null;
102      var authRect = authCards[i].getBoundingClientRect();
103      if (authRect.width > 0 && authRect.height > 0 && authStyle) {
104        authCardDetails = 'opacity ' + authStyle.opacity + ' · z ' + authStyle.zIndex + ' · visibility ' + authStyle.visibility + ' · transform ' + authStyle.transform;
105      }
106      if (isVisible(authCards[i])) visibleAuthCard = true;
107    }
108    var fallbackDisplay = 'no creado';
109    if (fallback && window.getComputedStyle) fallbackDisplay = window.getComputedStyle(fallback).display;
110    var cssBackdrop = false;
111    try {
112      cssBackdrop = !!(window.CSS && CSS.supports && (CSS.supports('backdrop-filter', 'blur(2px)') || CSS.supports('-webkit-backdrop-filter', 'blur(2px)')));
113    } catch (error) {}
114
115    var lines = [
116      'DIAGNÓSTICO SAFARI · ' + new Date().toLocaleTimeString(),
117      'Ruta: ' + (window.location.pathname || '/'),
118      'Documento: ' + document.readyState,
119      'Viewport: ' + window.innerWidth + 'x' + window.innerHeight + ' · DPR ' + (window.devicePixelRatio || 1),
120      'Montaje: ' + (root.getAttribute('data-rosgan-mounted') || 'pendiente'),
121      'Ruta app: ' + (root.getAttribute('data-public-route') || 'sin definir'),
122      'React global: ' + (window.React ? 'sí' : 'no'),
123      'ReactDOM global: ' + (window.ReactDOM ? 'sí' : 'no'),
124      'Firebase global: ' + (window.firebase ? 'sí' : 'no'),
125      'Tarjeta de acceso visible: ' + (visibleAuthCard ? 'sí' : 'no'),
126      'Detalle tarjeta: ' + authCardDetails,
127      'Respaldo estático: ' + fallbackDisplay,
128      'Backdrop filter: ' + (cssBackdrop ? 'sí' : 'no'),
129      '',
130      'RECURSOS',
131      resourceSummary(),
132      '',
133      'EVENTOS'
134    ];
135    if (!entries.length) lines.push('  Sin errores capturados.');
136    entries.forEach(function (entry) {
137      lines.push('  [' + entry.at + '] ' + entry.kind + ': ' + entry.message);
138    });
139    lines.push('', 'USER AGENT', redact(navigator.userAgent || 'no disponible'));
140    return lines.join('\n');
141  }
142
143  function render() {
144    if (output) output.textContent = diagnosticText();
145  }
146
147  function makeButton(label, action) {
148    var button = document.createElement('button');
149    button.type = 'button';
150    button.textContent = label;
151    button.style.cssText = 'min-height:34px;padding:0 11px;border:1px solid rgba(255,255,255,.3);border-radius:8px;background:rgba(255,255,255,.12);color:#fff;font:700 12px -apple-system,BlinkMacSystemFont,Arial,sans-serif;';
152    button.addEventListener('click', action);
153    return button;
154  }
155
156  function mountPanel() {
157    if (panel || !document.body) return;
158    var style = document.createElement('style');
159    style.textContent = '#rosgan-safari-debug{position:fixed;left:0;right:0;bottom:0;z-index:2147483647;max-height:58vh;box-sizing:border-box;display:flex;flex-direction:column;background:rgba(3,15,31,.97);color:#e8f4ff;border-top:2px solid #3dd5c6;box-shadow:0 -16px 42px rgba(0,0,0,.42);font-family:-apple-system,BlinkMacSystemFont,Arial,sans-serif;-webkit-text-size-adjust:100%}#rosgan-safari-debug.is-collapsed{max-height:none}#rosgan-safari-debug.is-collapsed pre{display:none}#rosgan-safari-debug header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:9px 10px;background:#073e6f}#rosgan-safari-debug header strong{font-size:13px;letter-spacing:.02em}#rosgan-safari-debug header div{display:flex;gap:6px}#rosgan-safari-debug pre{margin:0;padding:12px 12px max(16px,env(safe-area-inset-bottom));overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;color:#dbeafe;-webkit-overflow-scrolling:touch}';
160    document.head.appendChild(style);
161
162    panel = document.createElement('aside');
163    panel.id = 'rosgan-safari-debug';
164    panel.setAttribute('aria-label', 'Diagnóstico técnico de Safari');
165    var header = document.createElement('header');
166    var title = document.createElement('strong');
167    title.textContent = 'Diagnóstico Safari';
168    var actions = document.createElement('div');
169    actions.appendChild(makeButton('Actualizar', render));
170    actions.appendChild(makeButton('Copiar', function () {
171      var text = diagnosticText();
172      if (navigator.clipboard && navigator.clipboard.writeText) {
173        navigator.clipboard.writeText(text).then(function () { add('INFO', 'Diagnóstico copiado.'); }).catch(function () { add('AVISO', 'Safari no permitió copiar. Usá una captura.'); });
174      } else {
175        add('AVISO', 'Portapapeles no disponible. Usá una captura.');
176      }
177    }));
178    actions.appendChild(makeButton('Contraer', function (event) {
179      collapsed = !collapsed;
180      panel.className = collapsed ? 'is-collapsed' : '';
181      event.currentTarget.textContent = collapsed ? 'Abrir' : 'Contraer';
182    }));
183    header.appendChild(title);
184    header.appendChild(actions);
185    output = document.createElement('pre');
186    panel.appendChild(header);
187    panel.appendChild(output);
188    document.body.appendChild(panel);
189    add('INFO', 'Panel de diagnóstico iniciado.');
190  }
191
192  window.addEventListener('error', function (event) {
193    var target = event && event.target;
194    if (target && target !== window && (target.src || target.href)) {
195      add('RECURSO', (target.tagName || 'elemento') + ' no cargó: ' + safePath(target.src || target.href));
196      return;
197    }
198    add('ERROR', (event && event.message ? event.message : 'Error JavaScript') + (event && event.filename ? ' · ' + safePath(event.filename) + ':' + (event.lineno || 0) : ''));
199  }, true);
200
201  window.addEventListener('unhandledrejection', function (event) {
202    var reason = event && event.reason;
203    add('PROMESA', reason && reason.message ? reason.message : reason || 'Rechazo sin detalle');
204  });
205  window.addEventListener('rosgan:app-mounted', function () { add('INFO', 'Interfaz principal montada.'); });
206  document.addEventListener('DOMContentLoaded', function () { mountPanel(); add('INFO', 'DOM listo.'); });
207  window.addEventListener('load', function () { add('INFO', 'Ventana cargada.'); });
208  window.setInterval(render, 1500);
209  window.setTimeout(function () { add('CONTROL', 'Revisión a los 12 segundos.'); }, 12000);
210  window.__ROSGAN_SAFARI_DEBUG__ = { add: add, render: render, text: diagnosticText };
211})();
212</script>
vendor: 1 bytes, line 212
212
213<script>
214(() => {
215  const path = window.location.pathname.replace(/\/+$/, '') || '/';
216  const hashPath = (window.location.hash || '').replace(/^#/, '').split('?')[0].replace(/\/+$/, '');
217  const publicRoutes = new Set(['/lacassina', '/lacassina/estadisticas', '/lacassina/comercial', '/lacassina/precios', '/lacassina/tv', '/lacassina/tv/config']);
218  const publicPathCandidate = publicRoutes.has(hashPath) ? hashPath : (publicRoutes.has(path) ? path : '');
219  const isPublicCabanas = /^\/cabanas(?:\/(?!crear(?:\/|$))[^/]+(?:\/(?:lotes\/[^/]+|estadisticas|precios|tv))?)?$/i.test(path);
220  const isCabanaTv = /^\/cabanas\/[^/]+\/tv$/i.test(path);
221  const isCabanaTvConfig = /^\/cabanas\/[^/]+\/tv\/config$/i.test(path);
222  const lacassinaLotMatch = publicPathCandidate ? null : path.match(/^\/lacassina\/([^/]+)\/([^/]+)$/i);
223  const publicPath = publicPathCandidate || (lacassinaLotMatch ? '/lacassina' : path);
224  const isLacassinaStats = publicPath === '/lacassina/estadisticas';
225  const isLacassinaCommercial = publicPath === '/lacassina/comercial';
226  const isLacassinaPrices = publicPath === '/lacassina/precios';
227  const isLacassinaTv = publicPath === '/lacassina/tv';
228  const isLacassinaTvConfig = publicPath === '/lacassina/tv/config';
229  const isLacassinaLot = !!lacassinaLotMatch;
230  const isLacassinaPublic = publicPath === '/lacassina' || isLacassinaStats || isLacassinaCommercial || isLacassinaPrices || isLacassinaTv || isLacassinaTvConfig || isLacassinaLot;
231  const isLocalHost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
232  const isLocalMailingEntry = isLocalHost && (path === '/mailing-admin' || hashPath === '/mailing-admin');
233  window.__ROSGAN_PUBLIC_PATH__ = publicPath;
234  window.__ROSGAN_LOCAL_MAILING_ENTRY__ = isLocalMailingEntry;
235  window.__ROSGAN_PUBLIC_ADVANCE__ = isLacassinaPublic ? 'lacassina' : '';
236  window.__ROSGAN_PUBLIC_CABANAS__ = isPublicCabanas;
237  window.__ROSGAN_PUBLIC_ROUTE__ = isLacassinaStats ? 'lacassina-stats' : (isLacassinaCommercial ? 'lacassina-commercial' : (isLacassinaPrices ? 'lacassina-prices' : ((isLacassinaTv || isCabanaTv) ? 'cabana-tv' : ((isLacassinaTvConfig || isCabanaTvConfig) ? 'cabana-tv-config' : (isLacassinaPublic ? 'lacassina' : (isPublicCabanas ? 'cabanas-public' : 'app'))))));
238  window.__ROSGAN_PUBLIC_LOT_NUMBER__ = lacassinaLotMatch ? decodeURIComponent(lacassinaLotMatch[1]) : '';
239  window.__ROSGAN_PUBLIC_RP__ = lacassinaLotMatch ? decodeURIComponent(lacassinaLotMatch[2]) : '';
240  document.documentElement.dataset.publicAdvance = window.__ROSGAN_PUBLIC_ADVANCE__;
241  document.documentElement.dataset.publicRoute = window.__ROSGAN_PUBLIC_ROUTE__;
242  if (!isLacassinaPublic) {
243    let hasPrivilegedSession = false;
244    try {
245      const cached = JSON.parse(sessionStorage.getItem('rosgan:authenticatedApiUser') || 'null');
246      const apiUser = cached?.apiUser || {};
247      const roles = [apiUser.roleId, apiUser.roles, apiUser.role]
248        .flat(Infinity)
249        .map((role) => typeof role === 'object' ?(role?.id || role?.roleId || role?.name || role?.value || '') : role)
250        .map((role) => String(role || '').trim().toLowerCase());
251      hasPrivilegedSession = cached?.expiresAt > Date.now()
252        && roles.some((role) => ['admin', 'sysadmin', 'dev', 'administrador', 'administrador del sistema'].includes(role));
253    } catch {}
254    document.documentElement.dataset.rosganSessionHint = hasPrivilegedSession ?'authenticated' : 'guest';
255  }
256})();
257</script>
vendor: 1 bytes, line 257
257
258<script>
259(function () {
260  if (typeof window.globalThis === 'undefined') window.globalThis = window;
261  if (!Object.hasOwn) {
262    Object.hasOwn = function (object, property) {
263      return Object.prototype.hasOwnProperty.call(Object(object), property);
264    };
265  }
266  if (!window.queueMicrotask && window.Promise) {
267    window.queueMicrotask = function (callback) {
268      Promise.resolve().then(callback).catch(function (error) {
269        window.setTimeout(function () { throw error; }, 0);
270      });
271    };
272  }
273  if (!window.requestIdleCallback) {
274    window.requestIdleCallback = function (callback) {
275      var startedAt = Date.now();
276      return window.setTimeout(function () {
277        callback({
278          didTimeout: false,
279          timeRemaining: function () { return Math.max(0, 50 - (Date.now() - startedAt)); }
280        });
281      }, 1);
282    };
283  }
284  if (!window.cancelIdleCallback) window.cancelIdleCallback = window.clearTimeout;
285})();
286</script>
286
287<style id="rosgan-static-boot-style">
288#rosgan-static-boot{display:none}
289html:not([data-public-route]) #rosgan-static-boot,
290html[data-public-route="app"] #rosgan-static-boot{
291  position:fixed;
292  top:0;
293  right:0;
294  bottom:0;
295  left:0;
296  inset:0;
297  z-index:2147483646;
298  display:none;
299  place-items:center;
300  box-sizing:border-box;
301  min-height:100vh;
302  min-height:100svh;
303  padding:22px 18px;
304  padding:max(22px,env(safe-area-inset-top)) max(18px,env(safe-area-inset-right)) max(22px,env(safe-area-inset-bottom)) max(18px,env(safe-area-inset-left));
305  overflow:hidden;
306  background:#06264a url('/assets/auth/rosgan-angus-field.png') center/cover no-repeat;
307  font-family:Arial,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
308}
309html:not([data-public-route]) #rosgan-static-boot.is-visible,
310html[data-public-route="app"] #rosgan-static-boot.is-visible{display:grid}
311html[data-rosgan-mounted="true"] #rosgan-static-boot{display:none!important}
312html[data-rosgan-session-hint="authenticated"] #rosgan-static-boot{
313  place-items:start center;
314  background:#f4f7fb;
315}
316html[data-rosgan-session-hint="authenticated"] #rosgan-static-boot:before{display:none}
317#rosgan-static-boot:before{
318  content:"";
319  position:absolute;
320  top:0;
321  right:0;
322  bottom:0;
323  left:0;
324  inset:0;
325  background:linear-gradient(145deg,rgba(0,30,66,.88),rgba(0,79,130,.72) 48%,rgba(0,26,58,.9));
326}
327.rosgan-static-boot-card{
328  position:relative;
329  z-index:2;
330  width:360px;
331  max-width:100%;
332  width:min(360px,100%);
333  box-sizing:border-box;
334  display:grid;
335  justify-items:center;
336  gap:17px;
337  padding:30px 24px;
338  border:1px solid rgba(255,255,255,.24);
339  border-radius:24px;
340  background:rgba(0,28,61,.52);
341  -webkit-backdrop-filter:blur(18px);
342  backdrop-filter:blur(18px);
343  box-shadow:0 28px 72px rgba(0,10,30,.36);
344  color:#fff;
345  text-align:center;
346}
347.rosgan-static-boot-logo{
348  display:grid;
349  place-items:center;
350  width:112px;
351  height:112px;
352  border-radius:50%;
353  background:#fff;
354  box-shadow:0 15px 38px rgba(0,15,42,.28);
355}
356.rosgan-static-boot-logo img{display:block;width:82%;height:62%;object-fit:contain}
357.rosgan-static-boot-spinner{
358  width:30px;
359  height:30px;
360  box-sizing:border-box;
361  border:3px solid rgba(255,255,255,.28);
362  border-top-color:#58d4dc;
363  border-radius:50%;
364  animation:rosganStaticSpin .78s linear infinite;
365}
366#rosgan-static-boot.has-error .rosgan-static-boot-spinner{display:none}
367.rosgan-static-boot-card strong{font-size:20px;line-height:1.15}
368.rosgan-static-boot-card p{margin:-7px 0 0;color:#d7e9f6;font-size:13px;line-height:1.5}
369.rosgan-static-boot-retry{
370  min-height:46px;
371  padding:0 22px;
372  border:0;
373  border-radius:999px;
374  background:#fff;
375  color:#064b86;
376  font:800 14px Arial,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
377  cursor:pointer;
378  -webkit-appearance:none;
379  appearance:none;
380}
381.rosgan-static-guest{display:grid}
382.rosgan-static-session{display:none}
383html[data-rosgan-session-hint="authenticated"] .rosgan-static-guest{display:none}
384html[data-rosgan-session-hint="authenticated"] .rosgan-static-session{display:grid}
385.rosgan-static-login-fields{display:grid;width:100%;gap:12px}
386.rosgan-static-login-field,
387.rosgan-static-login-action{
388  min-height:48px;
389  box-sizing:border-box;
390  display:flex;
391  align-items:center;
392  padding:0 15px;
393  border:1px solid rgba(255,255,255,.24);
394  border-radius:12px;
395  color:#c8d8ea;
396  background:rgba(255,255,255,.1);
397  font-size:14px;
398  text-align:left;
399}
400.rosgan-static-login-action{justify-content:center;border-color:#fff;background:#fff;color:#063766;font-weight:800}
401.rosgan-static-login-google{justify-content:center;color:#fff;font-weight:700}
402.rosgan-static-session{
403  position:relative;
404  z-index:2;
405  width:min(1120px,100%);
406  gap:18px;
407  color:#09284a;
408}
409.rosgan-static-session-bar,
410.rosgan-static-session-hero,
411.rosgan-static-session-card{
412  box-sizing:border-box;
413  border:1px solid #dce7f2;
414  border-radius:20px;
415  background:#fff;
416  box-shadow:0 18px 44px rgba(27,63,101,.08);
417}
418.rosgan-static-session-bar{min-height:66px;display:flex;align-items:center;justify-content:space-between;padding:12px 20px}
419.rosgan-static-session-bar img{width:88px;height:38px;object-fit:contain}
420.rosgan-static-session-nav{display:flex;gap:9px}
421.rosgan-static-session-nav span{width:66px;height:28px;border-radius:999px;background:#edf3f8}
422.rosgan-static-session-hero{min-height:178px;padding:28px;background:linear-gradient(135deg,#052b54,#1266a9);border:0}
423.rosgan-static-session-hero:before{content:"";display:block;width:180px;height:18px;border-radius:999px;background:rgba(255,255,255,.92);box-shadow:0 34px 0 rgba(255,255,255,.28),0 68px 0 rgba(255,255,255,.18)}
424.rosgan-static-session-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}
425.rosgan-static-session-card{min-height:145px;padding:20px}
426.rosgan-static-session-card:before,.rosgan-static-session-card:after{content:"";display:block;height:13px;border-radius:999px;background:#dbe7f1}
427.rosgan-static-session-card:before{width:44%;margin-bottom:34px}.rosgan-static-session-card:after{width:72%;height:28px;background:#edf3f8}
428@keyframes rosganStaticSpin{to{transform:rotate(360deg)}}
429@media(max-width:600px){
430  html:not([data-public-route]) #rosgan-static-boot,
431  html[data-public-route="app"] #rosgan-static-boot{background-image:url('/assets/auth/rosgan-angus-field-vertical.png')}
432  .rosgan-static-boot-card{padding:28px 20px;border-radius:22px}
433  .rosgan-static-session-nav span:nth-child(n+4){display:none}
434  .rosgan-static-session-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
435  .rosgan-static-session-card{min-height:120px;padding:16px}
436  .rosgan-static-session-hero{min-height:150px;padding:22px}
437}
438@media(prefers-reduced-motion:reduce){.rosgan-static-boot-spinner{animation-duration:1.5s}}
439.roslot-op-row{grid-template-columns:minmax(0,1fr) auto auto;min-height:58px;padding:8px 10px}
440.roslot-op-topline{min-height:22px}.roslot-op-meta-line{display:flex;align-items:center;gap:10px;min-width:0;margin-top:3px}.roslot-op-meta-line p{margin:0;min-width:0}.roslot-op-meta-line .roslot-op-status{flex:none;margin:0}
441.roslot-op-alert{flex:none;width:auto;margin:0 0 0 auto;padding:4px 7px;border-radius:7px;box-shadow:0 4px 10px rgba(191,28,44,.2)}.roslot-op-alert i{font-size:12px}.roslot-op-alert strong{font-size:9px}.roslot-op-alert span{font-size:8px}
442.roslot-op-price{align-self:stretch;display:grid;grid-template-columns:auto;align-content:center;justify-items:end;gap:2px;min-width:128px;width:auto;margin:0;padding:6px 11px;border-radius:10px}.roslot-op-price span{font-size:8px}.roslot-op-price strong{font-size:27px;line-height:.95}.roslot-op-price em{font-size:8px}
443.roslot-op-row.is-winning .roslot-op-price,.roslot-op-row.is-seller .roslot-op-price{border-color:#bcded3;background:#f0faf6}.roslot-op-row.is-winning .roslot-op-price strong,.roslot-op-row.is-seller .roslot-op-price strong{color:#14745f}
444.roslot-op-actions button.is-hammer{display:inline-flex;align-items:center;gap:6px;border-color:#b86d00;background:linear-gradient(135deg,#c77a08,#a95b00);color:#fff;box-shadow:0 6px 14px rgba(169,91,0,.2)}
445.roslot-op-actions button.is-hammer:hover{border-color:#945000;background:linear-gradient(135deg,#b86d00,#944d00);color:#fff}
446.roslot-op-actions button.is-hammer:disabled{cursor:wait;color:#fff}
447@media(max-width:760px){.roslot-op-row{grid-template-columns:minmax(0,1fr) minmax(118px,auto);gap:7px;min-height:64px}.roslot-op-topline{flex-wrap:wrap}.roslot-op-alert{margin-left:0}.roslot-op-alert span{display:none}.roslot-op-price{min-width:108px}.roslot-op-price strong{font-size:24px}.roslot-op-actions{grid-column:1/-1}.roslot-op-meta-line{align-items:flex-start;flex-direction:column;gap:3px}}
448@supports (-webkit-touch-callout:none){
449  .rosgan-static-boot-card{
450    background:rgba(0,28,61,.9);
451    -webkit-backdrop-filter:none;
452    backdrop-filter:none;
453  }
454}
455.rosgan-tv-workspace{display:grid;grid-template-columns:minmax(290px,.8fr) minmax(0,1.2fr);gap:16px;margin-top:18px;align-items:start}.ros-tv-search{width:100%;box-sizing:border-box;padding:12px;border:1px solid #cad8e6;border-radius:10px;margin-bottom:12px}.ros-tv-lot-list{max-height:72vh;overflow:auto;display:grid;gap:6px;padding-right:5px}.ros-tv-lot-row{content-visibility:auto;contain-intrinsic-size:auto 76px;display:flex;align-items:center;gap:12px;text-align:left;border:1px solid #e0e8f0;background:#fff;padding:12px;border-radius:12px;cursor:pointer;color:#102f4b}.ros-tv-lot-row:hover{background:#f0f6fc}.ros-tv-lot-row.is-selected{border-color:#0a5ca8;background:#eaf4ff;box-shadow:inset 3px 0 #0a5ca8}.ros-tv-order{font-weight:900;font-size:21px;min-width:38px}.ros-tv-lot-copy{display:grid;gap:4px;flex:1;min-width:0}.ros-tv-lot-copy small{font-size:11px;color:#607890}.ros-tv-lot-copy strong{font-size:14px}.ros-tv-lot-copy>span{font-size:12px;color:#567086}.ros-tv-cons-mark{width:42px;height:42px;flex-shrink:0;display:grid;place-items:center;border-radius:9px;padding:6px;box-sizing:border-box}.ros-tv-cons-mark img{width:100%;height:100%;object-fit:contain}.ros-tv-editor{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.ros-tv-edit-field{display:grid;gap:7px;align-content:start;font-size:12px;font-weight:700;color:#526b82}.ros-tv-edit-field input,.ros-tv-edit-field textarea,.ros-tv-edit-field select{width:100%;box-sizing:border-box;min-height:40px;padding:10px;border:1px solid #cad8e6;border-radius:9px;font:inherit;color:#12334e;background:#fff}.ros-tv-breeds{display:flex;flex-wrap:wrap;gap:5px;max-height:160px;overflow:auto}.ros-tv-breeds button{border:1px solid #cad8e6;background:#fff;border-radius:7px;padding:6px;color:#34546d;cursor:pointer}.ros-tv-breeds .is-selected{background:#0a5ca8;color:#fff;border-color:#0a5ca8}.ros-tv-save-status{background:#edf5fb;color:#275576;padding:12px;border-radius:9px;font-size:12px}.ros-tv-retry{margin-top:16px;border:1px solid #cad8e6;padding:10px;border-radius:9px;background:#fff;color:#244d70;cursor:pointer}@media(max-width:800px){.rosgan-tv-workspace{grid-template-columns:1fr}.ros-tv-lot-list{max-height:360px}}@media(max-width:440px){.ros-tv-editor{grid-template-columns:1fr}}
456.ros-tv-catalog-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:15px}.rosgan-tv-config-card .ros-tv-catalog-heading h2{margin:0}.ros-tv-catalog-arrows{display:flex;gap:6px;flex-shrink:0}.ros-tv-catalog-arrows button{display:grid;place-items:center;width:36px;height:36px;padding:0;border:1px solid #cad8e6;border-radius:9px;background:#fff;color:#0a5ca8;font-size:18px;
456cursor:pointer}.ros-tv-catalog-arrows button:hover{background:#eaf4ff;border-color:#0a5ca8}.ros-tv-catalog-arrows button:focus-visible{outline:2px solid #0a5ca8;outline-offset:2px}
457
458.rd-company-dialog{width:min(590px,calc(100% - 32px));padding:0;border:0;border-radius:22px;box-shadow:0 28px 90px #001d3d40;font-family:inherit;max-height:calc(100dvh - 32px)}
459.rd-company-dialog::backdrop{background:#001d3d88;backdrop-filter:blur(4px)}
460.rd-company-dialog-header{display:flex;gap:14px;align-items:center;padding:24px;background:color-mix(in srgb,var(--company-color) 7%,white);border-bottom:1px solid #e5edf3}
461.rd-company-dialog-brand{display:grid;place-items:center;background:var(--company-color);width:56px;height:56px;flex-shrink:0;border-radius:15px;color:white;font-size:24px;font-weight:800}.rd-company-dialog-brand img{width:44px;height:44px;object-fit:contain}
462.rd-company-dialog-identity{display:grid;gap:6px;flex:1}.rd-company-dialog-eyebrow{font-size:10px;letter-spacing:.09em;color:#62748a;font-weight:700}.rd-company-dialog-identity strong{font-size:19px}
463.rd-company-dialog-dismiss{align-self:flex-start;border:1px solid #dbe5ed;background:white;border-radius:50%;width:32px;height:32px;font-size:23px;line-height:1;color:#52687b;cursor:pointer}
464.rd-company-dialog-body{padding:26px}.rd-company-dialog-body h2{font-size:22px;letter-spacing:-.5px;margin:0 0 8px}.rd-company-dialog-intro{margin:0 0 22px}
465.rd-company-dialog-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px}.rd-company-dialog-actions .rd-company-dialog-option{display:flex;flex-direction:column;align-items:flex-start;text-align:left;gap:16px;background:white;color:#002e5f;border:1px solid #dce5ee;border-radius:14px;padding:18px;font:inherit;transition:background .15s,border-color .15s,transform .15s}.rd-company-dialog-option:hover{border-color:var(--company-color);background:color-mix(in srgb,var(--company-color) 5%,white);transform:translateY(-2px)}.rd-company-dialog-option:focus-visible,.rd-company-dialog-dismiss:focus-visible{outline:3px solid var(--company-color);outline-offset:3px}
466.rd-company-dialog-symbol{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:color-mix(in srgb,var(--company-color) 10%,white)}.rd-company-dialog-symbol img{width:22px;height:22px}.rd-company-dialog-option-copy{display:flex;flex-direction:column;gap:8px;height:100%}.rd-company-dialog-option-copy strong{font-size:16px}.rd-company-dialog-option-copy>span{font-size:13px;line-height:1.5;color:#62748a}.rd-company-dialog-option-copy .rd-company-dialog-download{padding-top:12px;margin-top:auto;color:#002e5f;font-weight:750}.rd-company-dialog-format{margin-top:22px;padding-top:18px;border-top:1px solid #edf1f5;text-align:center;color:#62748a;font-size:12px}
467@media(max-width:480px){.rd-company-dialog-actions{grid-template-columns:1fr}.rd-company-dialog-actions .rd-company-dialog-option{flex-direction:row}.rd-company-dialog-symbol{flex-shrink:0}.rd-company-dialog-body{padding:20px}.rd-company-dialog-header{padding:20px}}
468</style>
469<style>
470.ros-tv-editor-heading{position:sticky;top:64px;z-index:8;background:#fff;padding:10px 0;border-bottom:1px solid #e5edf5;display:flex;align-items:center;justify-content:space-between;gap:12px}.ros-tv-editor-heading h2{margin:0}.ros-tv-save{display:grid;place-items:center;flex-shrink:0;width:38px;height:38px;border:1px solid #0a5ca8;border-radius:9px;background:#0a5ca8;color:#fff;cursor:pointer}.ros-tv-save:hover{background:#084a87}.ros-tv-save:focus-visible{outline:3px solid #93c5fd;outline-offset:3px}.ros-tv-save[aria-disabled="true"]{opacity:.4;cursor:default}
471</style>
472<style id="rosgan-clean-boot-style">
473 .ros-tv-lot-main{display:flex;align-items:center;gap:12px;flex:1;min-width:0;border:0;background:transparent;color:inherit;text-align:left;padding:0;cursor:pointer}.ros-tv-slot-buttons{display:flex;gap:3px}.ros-tv-slot-buttons button{width:27px;height:30px;border:1px solid #cad8e6;border-radius:6px;background:#fff;color:#0a5ca8;font-weight:800;cursor:pointer}.ros-tv-slot-buttons button.is-active{background:#0a5ca8;color:#fff}.ros-tv-slot-buttons button:disabled{opacity:.3;cursor:default}.ros-tv-multi-controls{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin:10px 0;font-size:12px;color:#526b82}.ros-tv-multi-controls button{border:1px solid #cad8e6;background:#edf5fb;color:#0a5ca8;padding:6px 9px;border-radius:7px;cursor:pointer}
474html[data-public-route="app"] x-dc{visibility:hidden}
475html[data-public-route="app"][data-rosgan-mounted="true"] x-dc{visibility:visible}
476html[data-public-route="app"] #rosgan-static-boot{
477  position:fixed;
478  inset:0;
479  z-index:2147483646;
480  display:grid!important;
481  place-items:center;
482  min-height:100vh;
483  min-height:100svh;
484  padding:0;
485  background:#f5f8fc;
486  opacity:1;
487  visibility:visible;
488  transition:opacity .14s ease,visibility 0s linear .14s;
489}
490html[data-public-route="app"][data-rosgan-mounted="true"] #rosgan-static-boot{
491  display:grid!important;
492  opacity:0;
493  visibility:hidden;
494  pointer-events:none;
495}
496#rosgan-static-boot .rosgan-static-boot-card{
497  width:auto;
498  padding:0;
499  border:0;
500  background:transparent;
501  box-shadow:none;
502  -webkit-backdrop-filter:none;
503  backdrop-filter:none;
504}
505html[data-rosgan-session-hint="authenticated"] #rosgan-static-boot .rosgan-static-guest{display:grid}
506#rosgan-static-boot .rosgan-static-boot-spinner,
507.rosgan-fast-loader-spinner{
508  display:block;
509  width:28px;
510  height:28px;
511  box-sizing:border-box;
512  border:3px solid #d9e5f0;
513  border-top-color:#075da8;
514  border-radius:50%;
515  animation:rosganStaticSpin .62s linear infinite;
516}
517#rosgan-static-boot [data-rosgan-boot-status],
518#rosgan-static-boot [data-rosgan-boot-detail],
519#rosgan-static-boot [data-rosgan-boot-retry]{display:none}
520#rosgan-static-boot.has-error .rosgan-static-boot-card{gap:14px;padding:26px;text-align:center;color:#082d54}
521#rosgan-static-boot.has-error [data-rosgan-boot-status],
522#rosgan-static-boot.has-error [data-rosgan-boot-detail],
523#rosgan-static-boot.has-error [data-rosgan-boot-retry]{display:block}
524.rosgan-fast-loader{
525  position:fixed;
526  inset:0;
527  z-index:2147483000;
528  display:grid;
529  place-items:center;
530  min-height:100vh;
531  min-height:100svh;
532  background:#f5f8fc;
533}
534@media(prefers-reduced-motion:reduce){
535  html[data-public-route="app"] #rosgan-static-boot{transition:none}
536  #rosgan-static-boot .rosgan-static-boot-spinner,
537  .rosgan-fast-loader-spinner{animation-duration:1.1s}
538}
539</style>
540<script defer src="/vendor/react.production.min.js?v=18.2.0"></script>
vendor: 1 bytes, line 540
540
541<script defer src="/vendor/react-dom.production.min.js?v=18.2.0"></script>
vendor: 1 bytes, line 541
541
542<script defer src="/vendor/firebase-app-compat.js?v=10.12.5"></script>
vendor: 1 bytes, line 542
542
543<script defer src="/vendor/firebase-auth-compat.js?v=10.12.5"></script>
vendor: 1 bytes, line 543
543
544<script defer src="/support.js?v=20260827-stable-inputs-8"></script>
544
545<link rel="preload" href="/assets/auth/options/success-1.json" as="fetch" type="application/json" crossorigin>
546<link rel="modulepreload" href="/vendor/dotlottie-wc.js?v=0.9.27">
547<script type="module" src="/vendor/dotlottie-bootstrap.js?v=0.9.27"></script>
547
548<style id="lacassina-public-stats-style">
549html[data-public-route="lacassina-stats"] body{
550  margin:0;
551  overflow-x:hidden;
552  background:#071f3d !important;
553}
554html[data-public-route="lacassina-stats"] x-dc,
555html[data-public-route="lacassina-stats"] #dc-root{
556  display:none !important;
557}
558#lacassina-stats-standalone{
559  display:none;
560  min-height:100vh;
561  color:#102033;
562  font-family:'Archivo',system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
563  background:
564    radial-gradient(circle at 16% 8%, rgba(26,112,185,.42), transparent 34rem),
565    radial-gradient(circle at 82% 12%, rgba(44,186,181,.20), transparent 30rem),
566    linear-gradient(180deg,#062340 0%,#0b4f88 36%,#f3f7fb 36%,#f3f7fb 100%);
567}
568html[data-public-route="lacassina-stats"] #lacassina-stats-standalone{
569  display:block;
570}
571.lc-stats-shell{
572  width:min(1180px, calc(100% - 32px));
573  margin:0 auto;
574  padding:28px 0 52px;
575}
576.lc-stats-hero{
577  position:relative;
578  overflow:hidden;
579  border-radius:28px;
580  padding:28px;
581  min-height:280px;
582  color:#fff;
583  background:linear-gradient(145deg,#061e3b 0%,#084673 52%,#062c52 100%);
584  box-shadow:0 28px 70px rgba(0,25,60,.28);
585}
586.lc-stats-hero:before{
587  content:"";
588  position:absolute;
589  inset:-28%;
590  pointer-events:none;
591  background:
592    linear-gradient(120deg,transparent 10%,rgba(26,169,210,.34) 28%,rgba(255,255,255,.11) 42%,rgba(26,184,173,.30) 56%,transparent 76%),
593    linear-gradient(40deg,rgba(5,30,72,.18),rgba(31,116,184,.28),rgba(5,30,72,.16));
594  background-size:180% 180%;
595  filter:blur(18px);
596  opacity:.86;
597  animation:lcStatsGlowShift 10s ease-in-out infinite alternate;
598}
599.lc-stats-hero:after{
600  content:"";
601  position:absolute;
602  inset:0;
603  pointer-events:none;
604  background:
605    linear-gradient(180deg,rgba(255,255,255,.10),transparent 34%),
606    linear-gradient(0deg,rgba(0,18,42,.30),transparent 48%);
607}
608@keyframes lcStatsGlowShift{
609  0%{transform:translate3d(-7%,-3%,0) scale(1);background-position:0% 44%}
610  100%{transform:translate3d(7%,4%,0) scale(1.04);background-position:100% 56%}
611}
612.lc-stats-hero-inner{
613  position:relative;
614  z-index:1;
615  display:grid;
616  grid-template-columns:1fr minmax(260px,360px);
617  gap:28px;
618  align-items:center;
619}
620.lc-stats-back{
621  position:relative;
622  z-index:1;
623  display:inline-flex;
624  align-items:center;
625  gap:8px;
626  width:max-content;
627  margin-bottom:22px;
628  padding:10px 14px;
629  border-radius:999px;
630  color:#dff1ff;
631  background:rgba(255,255,255,.10);
632  border:1px solid rgba(255,255,255,.16);
633  font-weight:800;
634  font:800 16px inherit;
635  cursor:pointer;
636  text-decoration:none;
637}
638.lc-stats-kicker{
639  margin:0 0 8px;
640  color:#a9d9ff;
641  font-size:12px;
642  font-weight:900;
643  letter-spacing:.14em;
644  text-transform:uppercase;
645}
646.lc-stats-hero h1{
647  margin:0;
648  max-width:780px;
649  font-size:clamp(34px,5vw,68px);
650  line-height:.94;
651  letter-spacing:0;
652}
653.lc-stats-subtitle{
654  margin:18px 0 0;
655  max-width:680px;
656  color:#dbeefe;
657  font-size:17px;
658  line-height:1.45;
659  font-weight:600;
660}
661.lc-stats-logo-card{
662  display:flex;
663  flex-direction:column;
664  align-items:center;
665  justify-content:center;
666  gap:18px;
667  min-height:178px;
668  padding:22px;
669  border-radius:22px;
670  background:rgba(255,255,255,.94);
671  border:1px solid rgba(255,255,255,.28);
672  box-shadow:0 20px 48px rgba(0,20,55,.22);
673}
674.lc-stats-main-logo{
675  max-width:100%;
676  max-height:118px;
677  object-fit:contain;
678}
679.lc-stats-logo-label{
680  color:#64748b;
681  font-size:11px;
682  font-weight:900;
683  letter-spacing:.10em;
684  text-transform:uppercase;
685}
686.lc-stats-guest-logos{
687  display:flex;
688  flex-wrap:wrap;
689  justify-content:center;
690  gap:10px;
691  width:100%;
692  padding-top:14px;
693  border-top:1px solid #e3edf7;
694}
695.lc-stats-guest-logo{
696  display:grid;
697  place-items:center;
698  width:86px;
699  height:44px;
700  padding:7px;
701  border-radius:12px;
702  background:#fff;
703  border:1px solid #e4edf6;
704}
705.lc-stats-guest-logo img{
706  max-width:100%;
707  max-height:30px;
708  object-fit:contain;
709}
710.lc-stats-guest-logo span{
711  overflow:hidden;
712  max-width:100%;
713  color:#5d7089;
714  font-size:10px;
715  font-weight:900;
716  text-align:center;
717  text-overflow:ellipsis;
718  white-space:nowrap;
719}
720.lc-stats-main{
721  margin-top:24px;
722  display:grid;
723  gap:18px;
724}
725.lc-stats-card{
726  border-radius:20px;
727  background:rgba(255,255,255,.96);
728  border:1px solid #dbe5f0;
729  box-shadow:0 18px 44px rgba(16,43,78,.08);
730}
731.lc-stats-section{
732  padding:22px;
733}
734.lc-stats-section-head{
735  display:flex;
736  justify-content:space-between;
737  align-items:flex-start;
738  gap:16px;
739  margin-bottom:18px;
740}
741.lc-stats-section h2{
742  margin:0;
743  color:#102033;
744  font-size:22px;
745  line-height:1.08;
746}
747.lc-stats-section small{
748  color:#71839c;
749  font-weight:800;
750}
751.lc-stats-status{
752  display:inline-flex;
753  align-items:center;
754  gap:8px;
755  padding:9px 12px;
756  border-radius:999px;
757  background:#e9f7f2;
758  color:#0c785b;
759  font-size:12px;
760  font-weight:900;
761  white-space:nowrap;
762}
763.lc-stats-status:before{
764  content:"";
765  width:8px;
766  height:8px;
767  border-radius:50%;
768  background:currentColor;
769}
770.lc-stats-grid{
771  display:grid;
772  grid-template-columns:repeat(4,minmax(0,1fr));
773  gap:14px;
774}
775.lc-stats-kpi{
776  min-height:122px;
777  padding:18px;
778  border-radius:18px;
779  background:linear-gradient(180deg,#fff,#eef5fc);
780  border:1px solid #dce7f2;
781}
782.lc-stats-kpi span{
783  display:block;
784  margin-bottom:12px;
785  color:#71839c;
786  font-size:12px;
787  font-weight:900;
788  letter-spacing:.08em;
789  text-transform:uppercase;
790}
791.lc-stats-kpi strong{
792  display:block;
793  color:#0b315d;
794  font-size:34px;
795  line-height:1;
796}
797.lc-stats-kpi em{
798  display:block;
799  margin-top:10px;
800  color:#60758f;
801  font-size:13px;
802  font-style:normal;
803  font-weight:700;
804}
805.lc-stats-two-col{
806  display:grid;
807  grid-template-columns:minmax(0,1.15fr) minmax(320px,.85fr);
808  gap:18px;
809}
810.lc-stats-bars{
811  display:grid;
812  gap:12px;
813}
814.lc-stat-row{
815  display:grid;
816  grid-template-columns:minmax(130px,220px) 1fr auto;
817  gap:12px;
818  align-items:center;
819}
820.lc-stat-row b{
821  color:#17314f;
822  font-size:14px;
823}
824.lc-stat-track{
825  overflow:hidden;
826  height:12px;
827  border-radius:999px;
828  background:#edf3fa;
829}
830.lc-stat-fill{
831  height:100%;
832  min-width:3px;
833  border-radius:999px;
834  background:linear-gradient(90deg,#0069b4,#20b8b0);
835}
836.lc-stat-row strong{
837  color:#0b315d;
838  font-size:14px;
839}
840.lc-stats-panel-list{
841  display:grid;
842  gap:10px;
843}
844.lc-stats-mini{
845  display:flex;
846  align-items:center;
847  justify-content:space-between;
848  gap:12px;
849  padding:13px 14px;
850  border-radius:14px;
851  background:#f3f7fb;
852  border:1px solid #dfebf5;
853}
854.lc-stats-mini b{
855  color:#132a46;
856}
857.lc-stats-mini span{
858  color:#5d7089;
859  font-size:13px;
860  font-weight:800;
861}
862.lc-stats-insight-grid{
863  display:grid;
864  grid-template-columns:repeat(3,minmax(0,1fr));
865  gap:14px;
866}
867.lc-stats-insight{
868  min-height:108px;
869  padding:18px 20px;
870  border-radius:18px;
871  background:#fff;
872  color:#102033;
873  border:1px solid #dbe5f0;
874  box-shadow:0 18px 38px rgba(16,43,78,.08);
875}
876.lc-stats-insight span{
877  display:block;
878  margin-bottom:10px;
879  color:#71839c;
880  font-size:11px;
881  font-weight:900;
882  letter-spacing:.10em;
883  text-transform:uppercase;
884}
885.lc-stats-insight strong{
886  display:block;
887  color:#0b315d;
888  font-size:22px;
889  line-height:1.08;
890}
891.lc-stats-insight em{
892  display:block;
893  margin-top:10px;
894  color:#65758a;
895  font-size:13px;
896  line-height:1.35;
897  font-style:normal;
898  font-weight:700;
899}
900.lc-stats-funnel{
901  display:grid;
902  grid-template-columns:repeat(4,minmax(0,1fr));
903  gap:12px;
904}
905.lc-stats-count-card{padding:16px;border-radius:16px;background:#f7fbff;border:1px solid #dce8f5}
906.lc-stats-count-card span{display:block;color:#71839c;font-size:11px;font-weight:900;letter-spacing:.09em;text-transform:uppercase}
907.lc-stats-count-card strong{display:block;margin-top:8px;color:#0b315d;font-size:30px;line-height:1}
908.lc-stats-count-card small{display:block;margin-top:8px;color:#65758a;font-size:12px;font-weight:800}
909.lc-stats-funnel-step{
910  display:grid;
911  grid-template-columns:150px 1fr 76px;
912  gap:12px;
913  align-items:center;
914}
915.lc-stats-funnel-step span{
916  color:#637994;
917  font-size:12px;
918  font-weight:900;
919  text-transform:uppercase;
920}
921.lc-stats-funnel-step strong{
922  color:#0b315d;
923  text-align:right;
924}
925.lc-stats-funnel-track{
926  height:28px;
927  overflow:hidden;
928  border-radius:999px;
929  background:#edf3fa;
930}
931.lc-stats-funnel-fill{
932  height:100%;
933  min-width:7px;
934  border-radius:999px;
935  background:linear-gradient(90deg,#0a5ca8,#18b8ad);
936}
937.lc-stats-alert-list{
938  display:grid;
939  gap:10px;
940}
941.lc-stats-alert{
942  display:flex;
943  gap:12px;
944  align-items:flex-start;
945  padding:14px;
946  border-radius:16px;
947  background:#fff8ed;
948  border:1px solid #fde7bf;
949  color:#6f4312;
950  font-size:13px;
951  font-weight:800;
952}
953.lc-stats-alert.is-good{
954  background:#edfdf7;
955  border-color:#c7f3e4;
956  color:#0f765d;
957}
958.lc-stats-alert i{
959  display:grid;
960  place-items:center;
961  width:24px;
962  height:24px;
963  flex:0 0 24px;
964  border-radius:50%;
965  background:currentColor;
966  color:#fff;
967  font-style:normal;
968  font-size:12px;
969}
970.lc-stats-media-grid{
971  display:grid;
972  grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
973  gap:12px;
974}
975.lc-stats-media-card{
976  display:grid;
977  grid-template-columns:116px minmax(0,1fr) auto;
978  gap:12px;
979  align-items:center;
980  padding:10px;
981  border-radius:16px;
982  background:#f7fbff;
983  border:1px solid #dce8f5;
984  color:inherit;
985  text-decoration:none;
986  cursor:pointer;
987  transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease;
988}
989.lc-stats-media-card:hover{
990  border-color:#9fc8e9;
991  box-shadow:0 12px 26px rgba(15,42,70,.1);
992  transform:translateY(-1px);
993}
994.lc-stats-media-thumb{
995  position:relative;
996  overflow:hidden;
997  width:116px;
998  aspect-ratio:16/9;
999  border-radius:12px;
1000  background:#0b315d;
1001}
1002.lc-stats-media-thumb img,
1003.lc-stats-media-thumb video{
1004  width:100%;
1005  height:100%;
1006  object-fit:cover;
1007  display:block;
1008}
1009.lc-stats-media-play{
1010  position:absolute;
1011  inset:auto 8px 8px auto;
1012  display:grid;
1013  place-items:center;
1014  width:28px;
1015  height:28px;
1016  border-radius:50%;
1017  background:rgba(255,255,255,.92);
1018  color:#0b315d;
1019  font-size:12px;
1020  font-weight:900;
1021}
1022.lc-stats-media-copy{
1023  min-width:0;
1024}
1025.lc-stats-media-copy span{
1026  display:block;
1027  color:#71839c;
1028  font-size:11px;
1029  font-weight:900;
1030  letter-spacing:.09em;
1031  text-transform:uppercase;
1032}
1033.lc-stats-media-copy strong{
1034  display:block;
1035  overflow:hidden;
1036  margin-top:4px;
1037  color:#102033;
1038  font-size:14px;
1039  line-height:1.2;
1040  text-overflow:ellipsis;
1041  white-space:nowrap;
1042}
1043.lc-stats-media-copy small{
1044  display:block;
1045  overflow:hidden;
1046  margin-top:4px;
1047  color:#65758a;
1048  font-size:12px;
1049  font-weight:800;
1050  text-overflow:ellipsis;
1051  white-space:nowrap;
1052}
1053.lc-stats-media-card > strong{
1054  color:#0b67b2;
1055  font-size:22px;
1056}
1057.lc-stats-range-tabs{
1058  display:flex;
1059  flex-wrap:wrap;
1060  gap:8px;
1061}
1062.lc-stats-range-tabs button{
1063  min-height:34px;
1064  padding:8px 12px;
1065  border:1px solid #cbd8e7;
1066  border-radius:999px;
1067  background:#fff;
1068  color:#31465f;
1069  font:900 12px inherit;
1070  cursor:pointer;
1071}
1072.lc-stats-range-tabs button.is-active{
1073  border-color:#0b67b2;
1074  background:#0b67b2;
1075  color:#fff;
1076}
1077.lc-stats-timeline{
1078  display:grid;
1079  gap:14px;
1080}
1081.lc-stats-chart{
1082  position:relative;
1083  overflow:hidden;
1084  min-height:220px;
1085  padding:0;
1086  border:1px solid #dce8f5;
1087  border-radius:18px;
1088  background:linear-gradient(180deg,#f7fbff,#fff);
1089}
1090.lc-stats-line-chart{display:block;width:100%;height:auto;min-height:220px}
1091.lc-stats-line-grid{stroke:#dce8f5;stroke-width:1}
1092.lc-stats-line-area{fill:url(#lcStatsLineArea)}
1093.lc-stats-line-path{fill:none;stroke:#0b67b2;stroke-width:4;stroke-linecap:round;stroke-linejoin:round}
1094.lc-stats-line-dot{fill:#fff;stroke:#0b67b2;stroke-width:3}
1095.lc-stats-line-label{fill:#60758f;font-size:11px;font-weight:900;text-anchor:middle}
1096.lc-stats-line-value{fill:#0b315d;font-size:11px;font-weight:900;text-anchor:middle}
1097.lc-stats-chart-bar{
1098  display:grid;
1099  grid-template-rows:1fr auto;
1100  gap:8px;
1101  min-width:0;
1102  height:100%;
1103}
1104.lc-stats-chart-bar i{
1105  align-self:end;
1106  display:block;
1107  min-height:4px;
1108  border-radius:999px 999px 4px 4px;
1109  background:linear-gradient(180deg,#18b8ad,#0a5ca8);
1110}
1111.lc-stats-chart-bar span{
1112  overflow:hidden;
1113  color:#60758f;
1114  font-size:10px;
1115  font-weight:900;
1116  text-align:center;
1117  text-overflow:ellipsis;
1118  white-space:nowrap;
1119}
1120.lc-stats-chart-summary{
1121  display:grid;
1122  grid-template-columns:repeat(3,minmax(0,1fr));
1123  gap:10px;
1124}
1125.lc-stats-chart-summary div{
1126  padding:12px 14px;
1127  border-radius:14px;
1128  background:#f3f7fb;
1129  border:1px solid #dfebf5;
1130}
1131.lc-stats-chart-summary span{display:block;color:#71839c;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
1132.lc-stats-chart-summary strong{display:block;margin-top:4px;color:#0b315d;font-size:20px}
1133.lc-stats-attribute-grid{
1134  display:grid;
1135  grid-template-columns:repeat(5,minmax(0,1fr));
1136  gap:12px;
1137}
1138.lc-stats-attribute-card{
1139  padding:15px;
1140  border-radius:16px;
1141  background:#f7fbff;
1142  border:1px solid #dce8f5;
1143}
1144.lc-stats-attribute-card span{display:block;color:#71839c;font-size:11px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
1145.lc-stats-attribute-card strong{display:block;margin-top:8px;color:#0b315d;font-size:22px;line-height:1.1}
1146.lc-stats-attribute-card small{display:block;margin-top:7px;color:#65758a;font-size:12px;font-weight:800}
1147.lc-stats-table-wrap{
1148  overflow:hidden;
1149  border-radius:16px;
1150  border:1px solid #dce7f2;
1151}
1152.lc-stats-table{
1153  width:100%;
1154  border-collapse:collapse;
1155}
1156.lc-stats-table th,
1157.lc-stats-table td{
1158  padding:14px 16px;
1159  border-bottom:1px solid #e5edf5;
1160  text-align:left;
1161}
1162.lc-stats-table th{
1163  background:#f1f6fb;
1164  color:#697d96;
1165  font-size:12px;
1166  font-weight:900;
1167  letter-spacing:.07em;
1168  text-transform:uppercase;
1169}
1170.lc-stats-table td{
1171  color:#142840;
1172  font-size:14px;
1173  font-weight:700;
1174}
1175.lc-stats-table tr:last-child td{
1176  border-bottom:0;
1177}
1178.lc-stats-signal-grid{
1179  display:grid;
1180  grid-template-columns:repeat(3,minmax(0,1fr));
1181  gap:12px;
1182}
1183.lc-stats-signal{
1184  display:flex;
1185  align-items:flex-start;
1186  gap:10px;
1187  padding:14px;
1188  border-radius:16px;
1189  border:1px solid #dce8f5;
1190  background:#f7fbff;
1191}
1192.lc-stats-signal i{
1193  display:inline-flex;
1194  align-items:center;
1195  justify-content:center;
1196  width:28px;
1197  height:28px;
1198  flex:0 0 28px;
1199  border-radius:999px;
1200  background:#dbeafe;
1201  color:#0b5ca8;
1202  font-style:normal;
1203  font-size:11px;
1204  font-weight:900;
1205}
1206.lc-stats-signal b{
1207  display:block;
1208  margin-bottom:3px;
1209  color:#102033;
1210  font-size:13px;
1211}
1212.lc-stats-signal span{
1213  display:block;
1214  color:#62708a;
1215  font-size:12px;
1216  line-height:1.35;
1217}
1218.lc-stats-signal.is-live{
1219  border-color:#b9ebd3;
1220  background:#ecfbf5;
1221}
1222.lc-stats-signal.is-live i{
1223  background:#c9f5df;
1224  color:#0f7a4e;
1225}
1226.lc-stats-signal.is-pending{
1227  border-color:#ffe0ad;
1228  background:#fff8ed;
1229}
1230.lc-stats-signal.is-pending i{
1231  background:#ffe8bd;
1232  color:#9a5a00;
1233}
1234.lc-stats-market-list{
1235  display:grid;
1236  gap:10px;
1237}
1238.lc-stats-market-item{
1239  display:flex;
1240  align-items:center;
1241  justify-content:space-between;
1242  gap:12px;
1243  padding:12px 14px;
1244  border-radius:14px;
1245  border:1px solid #dce8f5;
1246  background:#f7fbff;
1247}
1248.lc-stats-market-item b{
1249  color:#102033;
1250  font-size:13px;
1251}
1252.lc-stats-market-item span{
1253  color:#62708a;
1254  font-size:12px;
1255  text-align:right;
1256}
1257.lc-stats-score{
1258  display:inline-flex;
1259  align-items:center;
1260  justify-content:center;
1261  min-width:46px;
1262  padding:7px 10px;
1263  border-radius:999px;
1264  background:#e9f5ff;
1265  color:#005ea8;
1266  font-weight:900;
1267}
1268.lc-stats-action-pill{
1269  display:inline-flex;
1270  align-items:center;
1271  justify-content:center;
1272  padding:7px 10px;
1273  border-radius:999px;
1274  background:#edf3f8;
1275  color:#52637a;
1276  font-size:11px;
1277  font-weight:800;
1278  white-space:nowrap;
1279}
1280.lc-stats-loader{
1281  display:grid;
1282  gap:14px;
1283}
1284.lc-stats-skeleton{
1285  height:104px;
1286  border-radius:18px;
1287  background:linear-gradient(90deg,#e9f0f8,#f8fbff,#e9f0f8);
1288  background-size:220% 100%;
1289  animation:lcStatsPulse 1.2s ease-in-out infinite;
1290}
1291@keyframes lcStatsPulse{
1292  0%{background-position:0 0}
1293  100%{background-position:220% 0}
1294}
1295.lc-stats-error{
1296  padding:22px;
1297  border-radius:18px;
1298  background:#fff1f2;
1299  color:#9f1239;
1300  border:1px solid #fecdd3;
1301  font-weight:800;
1302}
1303@media (max-width:900px){
1304  .lc-stats-shell{width:min(100% - 20px, 680px); padding:14px 0 30px}
1305  .lc-stats-shell,.lc-stats-main,.lc-stats-main>*,.lc-stats-card,.lc-stats-section,.lc-stats-insight-grid,.lc-stats-two-col{min-width:0;max-width:100%;box-sizing:border-box}
1306  .lc-stats-main{grid-template-columns:minmax(0,1fr);overflow:hidden}
1307  .lc-stats-hero{padding:20px; border-radius:22px; min-height:auto}
1308  .lc-stats-hero-inner{grid-template-columns:1fr}
1309  .lc-stats-logo-card{min-height:126px}
1310  .lc-stats-main-logo{max-height:96px}
1311  .lc-stats-guest-logo{width:78px;height:40px}
1312  .lc-stats-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
1313  .lc-stats-two-col{grid-template-columns:1fr}
1314  .lc-stats-insight-grid{grid-template-columns:1fr}
1315  .lc-stats-signal-grid{grid-template-columns:1fr}
1316  .lc-stats-funnel{grid-template-columns:repeat(2,minmax(0,1fr))}
1317  .lc-stats-chart-summary{grid-template-columns:repeat(2,minmax(0,1fr))}
1318  .lc-stats-attribute-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
1319  .lc-stat-row{grid-template-columns:1fr auto; gap:8px}
1320  .lc-stat-track{grid-column:1 / -1; order:3}
1321  .lc-stats-funnel-step{grid-template-columns:1fr 64px}
1322  .lc-stats-funnel-track{grid-column:1 / -1; order:3}
1323}
1324@media (max-width:560px){
1325  .lc-stats-shell{width:100%; padding:0 0 24px}
1326  .lc-stats-hero{border-radius:0; padding:18px 16px 24px}
1327  .lc-stats-back{margin-bottom:18px}
1328  .lc-stats-subtitle{font-size:15px}
1329  .lc-stats-main{padding:0 10px}
1330  .lc-stats-grid{grid-template-columns:1fr}
1331  .lc-stats-media-card{grid-template-columns:94px minmax(0,1fr) auto}
1332  .lc-stats-media-thumb{width:94px}
1333  .lc-stats-funnel,.lc-stats-chart-summary,.lc-stats-attribute-grid{grid-template-columns:1fr}
1334  .lc-stats-section{padding:16px}
1335  .lc-stats-section-head{display:grid}
1336  .lc-stats-kpi{min-height:auto}
1337  .lc-stats-table th,
1338  .lc-stats-table td{padding:12px 10px; font-size:12px}
1339}
1340
1341/* La Cassina public statistics panel */
1342.lc-stats-empty{padding:16px;border:1px dashed rgba(13,72,124,.22);border-radius:18px;background:rgba(255,255,255,.72);display:grid;gap:4px;color:#65758a}
1343.lc-stats-empty strong{color:#10233f;font-size:14px}
1344.lc-stats-empty span{font-size:12px}
1345.lc-stat-card{border:1px solid rgba(13,72,124,.12);background:#fff;border-radius:20px;padding:18px;box-shadow:0 18px 38px rgba(20,50,92,.08);display:grid;gap:7px}
1346.lc-stat-card span,.lc-stats-mini-row span{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:#8090a6;font-weight:800}
1347.lc-stat-card strong{font-size:28px;color:#082b4f}
1348.lc-stat-card small,.lc-stats-mini-row small{font-size:12px;color:#63758d}
1349.lc-stats-bar-row{display:grid;grid-template-columns:minmax(120px,1.1fr) 2fr auto;gap:12px;align-items:center;padding:9px 0;border-bottom:1px solid rgba(13,72,124,.08)}
1350.lc-stats-bar-row:last-child{border-bottom:0}
1351.lc-stats-bar-row span{font-size:13px;font-weight:800;color:#173755}
1352.lc-stats-bar-row div{height:10px;border-radius:999px;background:#e8f0f8;overflow:hidden}
1353.lc-stats-bar-row i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,#0b67b2,#20b6c8)}
1354.lc-stats-bar-row strong{font-size:13px;color:#0b3155}
1355.lc-stats-alert-list{display:grid;gap:10px}
1356.lc-stats-alert{display:flex;gap:10px;align-items:flex-start;border:1px solid rgba(239,68,68,.18);background:#fff7f7;border-radius:16px;padding:12px}
1357.lc-stats-alert.is-good{border-color:rgba(24,128,97,.18);background:#f2fbf7}
1358.lc-stats-alert i{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:#ef4444;color:#fff;font-style:normal;font-weight:900;font-size:12px;flex:0 0 auto}
1359.lc-stats-alert.is-good i{background:#188061}
1360.lc-stats-alert strong{display:block;color:#10233f;font-size:13px}
1361.lc-stats-alert span{display:block;color:#65758a;font-size:12px;margin-top:2px}
1362.lc-stats-mini-row{display:grid;grid-template-columns:1fr auto;gap:4px 12px;padding:12px 0;border-bottom:1px solid rgba(13,72,124,.08)}
1363.lc-stats-mini-row:last-child{border-bottom:0}
1364.lc-stats-mini-row strong{font-size:20px;color:#0b67b2;text-align:right}
1365.lc-stats-mini-row small{grid-column:1 / -1}
1366.lc-stats-loader span{display:block;height:12px;border-radius:999px;background:linear-gradient(90deg,#eef4fb,#fff,#eef4fb);background-size:200% 100%;animation:lcStatsPulse 1.15s linear infinite;margin:10px 0}
1367.lc-stats-loader span:nth-child(2){width:74%}
1368.lc-stats-loader span:nth-child(3){width:52%}
1369@keyframes lcStatsPulse{to{background-position:-200% 0}}
1370@media (max-width:760px){.lc-stats-bar-row{grid-template-columns:1fr;gap:6px}.lc-stat-card strong{font-size:24px}}
1371@media (max-width:560px){
1372  html[data-public-route="lacassina-stats"],
1373  html[data-public-route="lacassina-stats"] body{overflow-x:hidden}
1374  html[data-public-route="lacassina-stats"] #lacassina-stats-standalone{overflow-x:hidden}
1375  .lc-stats-shell,.lc-stats-main,.lc-stats-card,.lc-stats-section{max-width:100%;box-sizing:border-box}
1376  .lc-stats-hero{overflow:hidden}
1377  .lc-stats-hero h1{font-size:clamp(30px,10vw,40px);line-height:1;overflow-wrap:anywhere}
1378  .lc-stats-kicker{font-size:10px;letter-spacing:.10em}
1379  .lc-stats-subtitle{font-size:14px;line-height:1.38}
1380  .lc-stats-logo-card{gap:12px;padding:16px;min-height:112px;border-radius:18px}
1381  .lc-stats-main-logo{max-height:76px}
1382  .lc-stats-guest-logos{gap:7px;padding-top:10px}
1383  .lc-stats-guest-logo{width:70px;height:36px;border-radius:10px}
1384  .lc-stats-section h2{font-size:20px}
1385  .lc-stats-section-head{gap:10px;margin-bottom:14px}
1386  .lc-stats-status{width:100%;display:grid;grid-template-columns:8px minmax(0,1fr);justify-content:stretch;align-items:center;white-space:normal;text-align:left;border-radius:16px;box-sizing:border-box;line-height:1.2}
1387  .lc-stats-status:before{grid-row:1 / span 3}
1388  .lc-stats-status span,.lc-stats-status strong,.lc-stats-status small{display:block;min-width:0}
1389  .lc-stats-status small{font-size:10px;opacity:.76}
1390  .lc-stats-grid{gap:10px}
1391  .lc-stats-kpi{padding:15px;border-radius:16px}
1392  .lc-stats-kpi span{margin-bottom:9px;font-size:10px}
1393  .lc-stats-kpi strong{font-size:30px}
1394  .lc-stats-kpi em{font-size:12px}
1395  .lc-stats-range-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:100%}
1396  .lc-stats-range-tabs button{padding:8px 6px;font-size:11px}
1397  .lc-stats-chart{min-height:176px}
1398  .lc-stats-line-chart{min-height:176px}
1399  .lc-stats-line-path{stroke-width:3.4}
1400  .lc-stats-line-dot{stroke-width:2.6}
1401  .lc-stats-line-label,.lc-stats-line-value{font-size:10px}
1402  .lc-stats-chart-summary{gap:8px}
1403  .lc-stats-count-card,.lc-stats-attribute-card{padding:14px}
1404  .lc-stats-count-card strong{font-size:28px}
1405  .lc-stats-media-grid{grid-template-columns:1fr}
1406  .lc-stats-media-card{grid-template-columns:84px minmax(0,1fr) 30px;gap:10px;padding:9px;border-radius:14px}
1407  .lc-stats-media-thumb{width:84px;border-radius:10px}
1408  .lc-stats-media-copy span{font-size:10px}
1409  .lc-stats-media-copy strong{font-size:13px}
1410  .lc-stats-media-copy small{font-size:11px}
1411  .lc-stats-media-card > strong{font-size:20px;text-align:right}
1412  .lc-stats-bar-row span,.lc-stat-row b{overflow-wrap:anywhere;line-height:1.2}
1413  .lc-stats-table-wrap{overflow:visible;border:0;border-radius:0}
1414  .lc-stats-table,.lc-stats-table tbody,.lc-stats-table tr,.lc-stats-table td{width:100%;box-sizing:border-box}
1415  .lc-stats-table thead{display:none}
1416  .lc-stats-table tbody{display:grid;gap:10px}
1417  .lc-stats-table tr{display:grid;width:100%;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px;align-items:stretch;padding:9px;border:1px solid #dce7f2;border-radius:14px;background:#fff;box-shadow:0 10px 22px rgba(16,43,78,.06)}
1418  .lc-stats-table td{display:grid;min-width:0;padding:0;border:0;text-align:left;font-size:12px}
1419  .lc-stats-table td:before{content:"";display:block;color:#71839c;font-size:9px;font-weight:900;letter-spacing:.05em;text-transform:uppercase}
1420  .lc-stats-table td:nth-child(1){grid-column:1 / 4;align-content:center}
1421  .lc-stats-table td:nth-child(1):before{content:""}
1422  .lc-stats-table td:nth-child(1) strong{display:block;overflow:hidden;color:#0b315d;font-size:14px;line-height:1.1;text-overflow:ellipsis;white-space:nowrap}
1423  .lc-stats-table td:nth-child(1) small{display:block;overflow:hidden;margin-top:2px;color:#65758a;font-size:11px;text-overflow:ellipsis;white-space:nowrap}
1424  .lc-stats-table td:nth-child(2){grid-column:4 / 6;align-content:center;overflow:hidden;color:#213b58;font-size:9px;font-weight:900;line-height:1.16;text-align:right;text-overflow:ellipsis;text-transform:uppercase}
1425  .lc-stats-table td:nth-child(2):before{content:"";display:none}
1426  .lc-stats-table td:nth-child(n+3){align-content:center;gap:1px;min-height:39px;padding:5px 3px;border:1px solid #e1ebf4;border-radius:10px;background:#f7fbff;text-align:center}
1427  .lc-stats-table td:nth-child(n+3):before{text-align:center}
1428  .lc-stats-table td:nth-child(3):before{content:"V. lote"}
1429  .lc-stats-table td:nth-child(4):before{content:"RP"}
1430  .lc-stats-table td:nth-child(5):before{content:"Vid"}
1431  .lc-stats-table td:nth-child(6):before{content:"Cont"}
1432  .lc-stats-table td:nth-child(7):before{content:"Comp"}
1433  .lc-stats-table td:nth-child(8):before{content:"Tpo"}
1434  .lc-stats-table td:nth-child(8){font-size:11px;line-height:1.1}
1435  .lc-stats-table td:nth-child(8) small{display:none}
1436  .lc-stats-table td strong,.lc-stats-table td small{overflow-wrap:anywhere}
1437}
1438</style>
1439<style>
1440.rg-lot-interest{display:flex;align-items:center;gap:5px;color:#075fac;font-size:11px;font-weight:700}.rg-lot-interest img{width:13px;height:13px}.rg-time-button{border:1px solid #ccdaea;border-radius:6px;background:#fff;color:#075fac;padding:4px 7px;cursor:pointer;margin-left:5px;font-size:12px}.rg-time-editor{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}.rg-time-editor input{width:100px;padding:6px;border:1px solid #b7cce2;border-radius:6px;font-variant-numeric:tabular-nums}.rg-airtime-panel{background:#fff;border:1px solid #dce7f2;border-radius:16px;padding:20px;margin:16px 0;color:#153858}.rg-airtime-panel header{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}.rg-airtime-panel header span{font-size:12px;color:#54718c}.rg-airtime-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin:20px 0}.rg-airtime-metrics small{display:block;color:#64809b;font-size:11px}.rg-airtime-metrics strong{display:block;font-size:25px;margin-top:5px;font-variant-numeric:tabular-nums}.rg-airtime-panel summary{cursor:pointer;font-weight:600;font-size:13px}.rg-airtime-panel p{font-size:11px;color:#6a8198;margin:14px 0 0}.rg-airtime-groups{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:14px}.rg-airtime-groups article{display:grid;gap:6px;background:#f3f7fb;padding:12px;border-radius:10px;font-size:12px}.rg-airtime-group-heading{display:flex;align-items:center;gap:10px;min-width:0}.rg-airtime-group-heading strong{overflow-wrap:anywhere}.rg-airtime-group-logo{display:flex;align-items:center;justify-content:center;flex:0 0 36px;width:36px;height:36px;border-radius:50%;color:#fff;font-weight:800;overflow:hidden}.rg-airtime-group-logo img{display:block;width:28px;height:28px;object-fit:contain}.rg-airtime-groups article small{color:#6a8198}.rg-airtime-lot{color:#075fac!important;font-variant-numeric:tabular-nums}@media(max-width:650px){.rg-airtime-metrics{grid-template-columns:repeat(2,1fr)}.rg-airtime-groups{grid-template-columns:1fr}}
1441</style><link rel="stylesheet" href="/assets/rosgan-action-buttons.css"><link rel="stylesheet" href="/assets/filming-assistant.css">
1441<script defer src="/assets/filming-assistant.js"></script>
1441<script defer src="/assets/raw-video-batch.js?v=multipart-1"></script>
1441<script defer src="/assets/raw-video-compression.js"></script>
1441<script defer src="/assets/field-mode.js?v=multipart-1"></script>
1441<script defer src="/assets/multipart-video.js?v=1"></script>
1441<script defer src="/assets/video-upload-queue.js?v=multipart-1"></script>
1441
1442<style>
1443.rg-commercial-page .rg-commercial-row{content-visibility:auto;contain-intrinsic-size:auto 145px}.rg-commercial-progress{padding:18px 20px;margin:16px 0;background:#fff;border:1px solid #dce7f2;border-radius:16px;color:#153858}.rg-commercial-progress header{display:flex;justify-content:space-between;gap:12px;margin-bottom:14px}.rg-commercial-progress header span{font-size:12px;color:#65758a}.rg-progress-track{display:flex;height:16px;border-radius:999px;overflow:hidden;background:#edf2f7}.rg-progress-track sc-for{display:contents}.rg-progress-segment{display:block;height:100%;flex-shrink:0;transition:width .35s ease}.rg-progress-legend{display:flex;flex-wrap:wrap;gap:12px 24px;margin-top:13px;font-size:12px}.rg-progress-legend>span,.rg-progress-legend sc-for>span{display:inline-flex;align-items:center;gap:6px}.rg-progress-legend i{width:9px;height:9px;border-radius:50%;flex-shrink:0}.rg-progress-legend strong{font-variant-numeric:tabular-nums}.rg-progress-legend small{color:#6a8198}.rg-commercial-page .rosgan-ops-status.is-no-vendido{background:#fff0f0;color:#c03434}.rg-commercial-page .rosgan-ops-status.is-levantado{background:#edf1f5;color:#526579}@media(prefers-reduced-motion:reduce){.rg-progress-segment{transition:none}}
1444</style><style>.rtv-china-badge{position:absolute;z-index:4;bottom:calc(100% + .45vh);left:0;display:flex;align-items:center;gap:.7vw;padding:.8vh 1vw;background:#c9242c;color:#fff;border-radius:.6vh;font:800 2vh Arial,Helvetica,sans-serif;text-transform:uppercase;letter-spacing:.035em;box-shadow:0 2px 6px #00142830}.rtv-lower:has(.rtv-observations) .rtv-china-badge{bottom:calc(100% + 4.3vh)}.rtv-china-badge img{display:block;width:3.2vh;height:2.4vh;object-fit:contain}.rtv-china-badge .sc-interp{font:inherit;color:inherit}</style><link rel="stylesheet" href="/assets/lot-destination.css">
1445<script src="/assets/remate-filters.js?v=eligibility-1"></script>
1445
1446<style>
1447.rd-filter-panel{margin-top:14px;padding:20px;background:linear-gradient(180deg,#fff 0%,#f8fbff 100%);border:1px solid #d6e4f1;border-radius:20px;box-shadow:0 14px 34px rgba(0,55,103,.08)}
1448.rd-filter-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-bottom:15px;border-bottom:1px solid #e5edf5}.rd-filter-title{display:flex;align-items:center;gap:10px;color:#073d70}.rd-filter-title>span:first-child{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:#e9f4ff}.rd-filter-title img{width:17px;height:17px}.rd-filter-title strong{font-size:15px}.rd-filter-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:#0865ad;color:#fff;font-size:10px;font-weight:900}.rd-filter-clear{display:inline-flex;align-items:center;gap:7px;padding:8px 11px;border:1px solid #d6e4f1;border-radius:10px;background:#fff;color:#0965aa;cursor:pointer;font:750 11px inherit;transition:.16s}.rd-filter-clear:hover{border-color:#8bb8db;background:#f0f8ff}.rd-filter-clear img{width:14px;height:14px}
1449.rd-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:16px}.rd-filter-group{min-width:0;padding:13px;border:1px solid #e0eaf3;border-radius:15px;background:rgba(255,255,255,.9)}.rd-filter-group-head{display:flex;align-items:center;gap:8px;margin-bottom:10px;color:#466984}.rd-filter-group-icon{display:grid;place-items:center;width:28px;height:28px;flex:none;border-radius:8px;background:#edf6ff}.rd-filter-group-icon img{width:15px;height:15px}.rd-filter-group-head strong{font-size:11px;letter-spacing:.025em}.rd-filter-options{display:flex;align-content:flex-start;flex-wrap:wrap;gap:7px;max-height:132px;overflow:auto;padding:1px 4px 3px 1px;scrollbar-width:thin;scrollbar-color:#b6c9da transparent}.rd-filter-chip{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:5px 9px;border:1px solid #d1dfec;border-radius:10px;background:#fff;color:#375875;font:600 11px inherit;text-align:left;cursor:pointer;transition:transform .15s,box-shadow .15s,border-color .15s,background .15s}.rd-filter-chip:hover{transform:translateY(-1px);border-color:#80afd2;box-shadow:0 5px 12px rgba(4,70,124,.09)}.rd-filter-chip[aria-pressed="true"]{border-color:#075c9e;background:#075c9e;color:#fff;box-shadow:0 5px 13px rgba(0,65,119,.18)}.rd-filter-chip-logo{display:grid;place-items:center;flex:none;width:23px;height:23px;padding:3px;box-sizing:border-box;border-radius:6px}.rd-filter-chip-logo img{width:100%;height:100%;object-fit:contain}.rd-filter-chip-count{display:inline-grid;place-items:center;min-width:18px;height:18px;padding:0 4px;border-radius:999px;background:#edf4fa;color:#51708c;font-size:9px;font-weight:900}.rd-filter-chip[aria-pressed="true"] .rd-filter-chip-count{background:#ffffff26;color:#fff}
1450@media(max-width:1050px){.rd-filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.rd-filter-panel{padding:14px}.rd-filter-grid{grid-template-columns:1fr}.rd-filter-head{align-items:flex-start}.rd-filter-group{padding:11px}}
1451</style>
1452<style>
1453.rosgan-tv-output .rtv-lower{transition:visibility 0s linear .45s}.rosgan-tv-output.has-name-strap .rtv-lower{visibility:hidden;transition-delay:0s}
1454.rtv-name-strap{position:absolute;left:25%;bottom:5vh;width:50%;height:16.6667vh;box-sizing:border-box;z-index:80;display:flex;align-items:center;padding:2vh 2.4vw;gap:2vw;background:linear-gradient(115deg,#001f43 0%,#003c74 62%,#0c6898 100%);border:1px solid #a8e8ff55;border-radius:1.5vh;box-shadow:0 1.2vh 3vh #001b3555;color:white;overflow:hidden;pointer-events:none;opacity:0;transform:translateY(3vh) scale(.97);visibility:hidden;transition:opacity .4s ease,transform .45s cubic-bezier(.22,1,.36,1),visibility 0s linear .45s}
1455.rtv-name-strap.is-on{opacity:1;transform:translateY(0) scale(1);visibility:visible;transition-delay:0s;animation:rtv-name-enter .5s cubic-bezier(.22,1,.36,1) both}
1456.rtv-name-strap:before{content:'';position:absolute;inset:0;background:linear-gradient(120deg,transparent 65%,#ffffff08 65%,#ffffff08 78%,transparent 78%);pointer-events:none}.rtv-name-strap:after{content:'';position:absolute;bottom:0;left:0;width:100%;height:.55vh;background:linear-gradient(90deg,#19a8c2,#96f0f5 50%,#147ab8)}
1457.rtv-name-emblem{flex:0 0 5.3vw;display:grid;place-items:center;padding-right:1.8vw;border-right:1px solid #bdd9ee55;height:9vh}.rtv-name-emblem img{width:100%;max-height:8vh;object-fit:contain;filter:brightness(0) invert(1)}.rtv-name-copy{flex:1;min-width:0;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;gap:.9vh}.rtv-name-strap .rtv-name-title{color:#fff;font:900 4.4vh/1.08 Archivo,Arial,sans-serif;letter-spacing:-.035em;white-space:nowrap;display:block;flex:none;overflow:hidden}.rtv-name-strap .rtv-name-subtitle{font:500 2.65vh/1.15 Archivo,Arial,sans-serif;color:#bde9f4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1458.rtv-name-strap .rtv-name-copy .sc-interp{font:inherit;color:inherit;letter-spacing:inherit;line-height:inherit;white-space:inherit}
1459@keyframes rtv-name-enter{from{opacity:0;transform:translateY(3vh) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
1460@media(prefers-reduced-motion:reduce){.rtv-name-strap,.rtv-name-strap.is-on{animation:none;transition:none}.rosgan-tv-output .rtv-lower{transition:none}}
1461.rtv-name-editor{display:flex;gap:10px;flex-wrap:wrap;margin:14px 0}.rtv-name-editor input{flex:1;min-width:170px;padding:11px;border:1px solid #cbddea;border-radius:9px;font:inherit}.rtv-name-item{display:flex;align-items:center;gap:10px;padding:12px 0;border-top:1px solid #e3edf5}.rtv-name-item div{flex:1}.rtv-name-item small{display:block;color:#68839a;margin-top:4px}.rtv-name-item button,.rtv-name-editor button{border:1px solid #cbddea;background:#f4f8fc;color:#004276;border-radius:9px;padding:10px;cursor:pointer;font:700 12px inherit}
1462</style>
1462<script src="/assets/rosgan-name-fit.js"></script>
vendor: 1 bytes, line 1462
1462
1463<script src="/assets/rosgan-countdown.js"></script>
1463<style>.rosgan-tv-output .rg-full-plate{z-index:100;background:linear-gradient(125deg,#002b54,#07538b,#003451);animation:none;backdrop-filter:none}.rg-full-plate:before,.rg-full-plate:after{animation:none;filter:none}.rg-full-plate .tv-waiting-frame{backdrop-filter:none}.rg-full-plate .tv-waiting-logos{display:flex;flex-wrap:wrap;justify-content:center;gap:2vw}.rg-full-plate .tv-waiting-logo{min-height:7vh;width:6vw;animation:none}.rg-full-plate .tv-waiting-logo img{max-height:7vh}.rg-full-plate .tv-waiting-logo.is-rosgan{width:17vw}.rg-full-plate .sc-interp{color:inherit;font:inherit}</style><style>
1464html[data-rosgan-video-hidden="true"],html[data-rosgan-video-hidden="true"] body,html[data-rosgan-video-hidden="true"] #dc-root,html[data-rosgan-video-hidden="true"] .sc-host,html[data-rosgan-video-hidden="true"] .sc-host>div{background:transparent!important}
1465.rosgan-tv-output.is-video-off{background:transparent!important}
1466.rosgan-tv-output.is-video-off .rtv-brand-cons,.rosgan-tv-output.is-video-off .rtv-brand-name,.rosgan-tv-output.is-video-off .ros-tv-air-pane .rtv-brand{display:none!important}
1467.rosgan-tv-output.is-video-off .rtv-brand-rosgan{border-radius:.55vw}
1468.rosgan-tv-output.is-video-off video,.rosgan-tv-output.is-video-off .rtv-lower,.rosgan-tv-output.is-video-off .rosgan-tv-fallback,.rosgan-tv-output.is-video-off .rosgan-tv-no-lot{visibility:hidden!important;transition:none!important}
1469</style><link rel="stylesheet" href="/assets/app-mobile.css"><link rel="stylesheet" href="/assets/cabana-video-fit.css?v=2">
1470<link rel="stylesheet" href="/assets/cabana-auction-list.css">
1471<script src="/assets/feria-youtube-control.js" defer></script>
1471
1472</head>
1473<body>
1474<section id="lacassina-stats-standalone" aria-label="Estadisticas publicas La Cassina">
1475  <div class="lc-stats-shell">
1476    <header class="lc-stats-hero">
1477      <a class="lc-stats-back" href="/lacassina"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</a>
1478      <div class="lc-stats-hero-inner">
1479        <div>
1480          <p class="lc-stats-kicker">Rosgan Cabanas · Inteligencia comercial</p>
1481          <h1>Centro de estadisticas La Cassina</h1>
1482          <p class="lc-stats-subtitle" id="lc-stats-auction-title">Cargando informacion del remate...</p>
1483        </div>
1484        <div class="lc-stats-logo-card" id="lc-stats-logo-card">
1485          <div class="lc-stats-skeleton" style="width:100%;height:132px"></div>
1486        </div>
1487      </div>
1488    </header>
1489    <main class="lc-stats-main">
1490      <section class="lc-stats-card lc-stats-section">
1491        <div class="lc-stats-section-head">
1492          <div>
1493            <small>PANEL EJECUTIVO</small>
1494            <h2>Lectura rapida del remate</h2>
1495          </div>
1496          <span class="lc-stats-status" id="lc-stats-status">Cargando datos</span>
1497        </div>
1498        <div class="lc-stats-grid" id="lc-stats-kpis">
1499          <div class="lc-stats-skeleton"></div>
1500          <div class="lc-stats-skeleton"></div>
1501          <div class="lc-stats-skeleton"></div>
1502          <div class="lc-stats-skeleton"></div>
1503          <div class="lc-stats-skeleton"></div>
1504          <div class="lc-stats-skeleton"></div>
1505        </div>
1506      </section>
1507      <section class="lc-stats-insight-grid" id="lc-stats-insights">
1508        <article class="lc-stats-insight"><span>Insight</span><strong>Cargando analisis...</strong><em>Esperando datos del catalogo</em></article>
1509        <article class="lc-stats-insight"><span>Riesgo</span><strong>Cargando cobertura...</strong><em>Control de imagenes, videos y datos</em></article>
1510        <article class="lc-stats-insight"><span>Accion</span><strong>Preparando prioridades...</strong><em>Lotes para revisar primero</em></article>
1511      </section>
1512      <section class="lc-stats-card lc-stats-section">
1513        <div class="lc-stats-section-head">
1514          <div>
1515            <small>ACTIVIDAD EN EL TIEMPO</small>
1516            <h2>Evolucion de señales</h2>
1517          </div>
1518          <div class="lc-stats-range-tabs" id="lc-stats-range-tabs" aria-label="Filtro temporal de estadisticas"></div>
1519        </div>
1520        <div class="lc-stats-timeline" id="lc-stats-timeline">
1521          <div class="lc-stats-skeleton"></div>
1522          <div class="lc-stats-skeleton"></div>
1523          <div class="lc-stats-skeleton"></div>
1524        </div>
1525      </section>
1526      <section class="lc-stats-card lc-stats-section">
1527        <div class="lc-stats-section-head">
1528          <div>
1529            <small>EMBUDO</small>
1530            <h2>Estado del catalogo</h2>
1531          </div>
1532        </div>
1533        <div class="lc-stats-funnel" id="lc-stats-funnel">
1534          <div class="lc-stats-skeleton"></div>
1535          <div class="lc-stats-skeleton"></div>
1536          <div class="lc-stats-skeleton"></div>
1537        </div>
1538      </section>
1539      <section class="lc-stats-two-col">
1540        <div class="lc-stats-card lc-stats-section">
1541          <div class="lc-stats-section-head">
1542            <div>
1543              <small>DISTRIBUCION DE INTERACCIONES</small>
1544              <h2>Interacciones por categoria</h2>
1545              <p>Suma vistas de lote, RPs vistos, videos e imagenes.</p>
1546            </div>
1547          </div>
1548          <div class="lc-stats-bars" id="lc-stats-category-bars">
1549            <div class="lc-stats-skeleton"></div>
1550            <div class="lc-stats-skeleton"></div>
1551          </div>
1552        </div>
1553        <div class="lc-stats-card lc-stats-section">
1554          <div class="lc-stats-section-head">
1555            <div>
1556              <small>CABANAS</small>
1557              <h2>Participacion</h2>
1558            </div>
1559          </div>
1560          <div class="lc-stats-panel-list" id="lc-stats-cabana-list">
1561            <div class="lc-stats-skeleton"></div>
1562          </div>
1563        </div>
1564      </section>
1565      <section class="lc-stats-card lc-stats-section">
1566        <div class="lc-stats-section-head">
1567          <div>
1568            <small>AUDIOVISUAL</small>
1569            <h2>Top 10 imagenes y videos</h2>
1570          </div>
1571        </div>
1572        <div class="lc-stats-panel-list" id="lc-stats-media-list">
1573          <div class="lc-stats-skeleton"></div>
1574        </div>
1575      </section>
1576      <section class="lc-stats-card lc-stats-section">
1577        <div class="lc-stats-section-head">
1578          <div>
1579            <small>GENETICA Y MERCADO</small>
1580            <h2>Atributos del catalogo</h2>
1581          </div>
1582        </div>
1583        <div class="lc-stats-market-list" id="lc-stats-genetics-list">
1584          <div class="lc-stats-skeleton"></div>
1585        </div>
1586      </section>
1587      <section class="lc-stats-card lc-stats-section">
1588        <div class="lc-stats-section-head">
1589          <div>
1590            <small>RANKING DE INTERES</small>
1591            <h2>Score operativo por lote</h2>
1592            <p>Ordenado por vistas, RPs vistos, reproducciones reales, imagenes, contactos y tiempo registrado.</p>
1593          </div>
1594        </div>
1595        <div class="lc-stats-table-wrap">
1596          <table class="lc-stats-table">
1597            <thead>
1598              <tr><th>Lote / RP</th><th>Categoria</th><th>Visitas lote</th><th>Vistas RP</th><th>Videos</th><th>Contactos</th><th>Compartidos</th><th>Tiempo prom. RP</th></tr>
1599            </thead>
1600            <tbody id="lc-stats-top-lots">
1601              <tr><td colspan="8">Cargando lotes...</td></tr>
1602            </tbody>
1603          </table>
1604        </div>
1605      </section>
1606    </main>
1607  </div>
1608</section>
1609<section id="rosgan-static-boot" role="status" aria-live="polite">
1610  <div class="rosgan-static-boot-card rosgan-static-guest">
1611    <span class="rosgan-static-boot-spinner" aria-hidden="true"></span>
1612    <strong data-rosgan-boot-status>No pudimos completar el inicio.</strong>
1613    <p data-rosgan-boot-detail>Reintentá para renovar la sesión de forma segura.</p>
1614    <button class="rosgan-static-boot-retry" data-rosgan-boot-retry type="button" hidden onclick="window.location.reload()">Reintentar</button>
1615  </div>
1616</section>
1617<script>
1618(function () {
1619  window.__ROSGAN_STATIC_BOOT_TIMER__ = window.setTimeout(function () {
1620    if (document.documentElement.getAttribute('data-rosgan-mounted') === 'true') return;
1621    var fallback = document.getElementById('rosgan-static-boot');
1622    if (!fallback) return;
1623    fallback.hidden = false;
1624    fallback.classList.add('is-visible');
1625    if (fallback.className.indexOf('has-error') === -1) fallback.className += ' has-error';
1626    var status = fallback.querySelector('[data-rosgan-boot-status]');
1627    var detail = fallback.querySelector('[data-rosgan-boot-detail]');
1628    var retry = fallback.querySelector('[data-rosgan-boot-retry]');
1629    if (status) status.textContent = 'No pudimos completar el inicio en este navegador.';
1630    if (detail) detail.textContent = 'Reintentá para renovar la sesión de forma segura.';
1631    if (retry) retry.hidden = false;
1632  }, 15000);
1633})();
1634</script>
1634
1635<x-dc>
1636<helmet>
1637<link rel="preconnect" href="https://fonts.googleapis.com">
1638<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
1639<script src="/vendor/proj4.js?v=2.11.0"></script>
vendor: 1 bytes, line 1639
1639
1640<script src="./vendor/Sortable.min.js"></script>
1640
1641<style>
1642html, body { margin:0; width:100%; max-width:100%; min-height:100%; overflow-x:hidden; overflow-x:clip; background:#f4f7fb; font-family:'Archivo',sans-serif; -webkit-font-smoothing:antialiased; }
1643x-dc, #dc-root { display:block; width:100%; max-width:100%; min-width:0; overflow-x:hidden; overflow-x:clip; }
1644[style*="position:absolute; inset:0"], [style*="position:fixed; inset:0"] { top:0; right:0; bottom:0; left:0; }
1645.rosgan-auth-screen{position:relative;isolation:isolate;background:#001d3d;overflow:visible!important}
1646.rosgan-auth-background{position:fixed!important;inset:0!important;z-index:0!important;pointer-events:none}
1647.rosgan-auth-shade{position:fixed!important;inset:0!important;z-index:1!important;pointer-events:none}
1648.rosgan-auth-card{
1649  z-index:5!important;
1650  max-width:calc(100vw - 32px)!important;
1651  box-sizing:border-box!important;
1652  opacity:1!important;
1653  visibility:visible!important;
1654  animation:none!important;
1655  -webkit-transform:translateZ(0)!important;
1656  transform:translateZ(0)!important;
1657}
1658.rosgan-auth-logo{
1659  display:block!important;
1660  width:132px!important;
1661  height:auto!important;
1662  max-width:46vw!important;
1663  object-fit:contain!important;
1664  object-position:center!important;
1665  align-self:center!important;
1666  flex:0 0 auto!important;
1667  margin-left:auto!important;
1668  margin-right:auto!important;
1669}
1670.rosgan-account-help-launcher{
1671  width:100%; border:1px solid rgba(154,219,255,.48); border-radius:13px; padding:12px 14px;
1672  background:rgba(18,139,207,.18); color:#fff; font:800 13px/1.35 inherit; cursor:pointer;
1673  display:flex; align-items:center; justify-content:center; gap:9px; text-align:center;
1674  transition:transform 160ms ease,background 160ms ease,border-color 160ms ease;
1675}
1676.rosgan-account-help-launcher:hover{transform:translateY(-1px);background:rgba(18,139,207,.3);border-color:rgba(189,231,255,.75)}
1677.rosgan-account-assistant{
1678  display:grid; gap:12px; padding:15px; border-radius:16px; color:#07345e;
1679  background:linear-gradient(145deg,rgba(255,255,255,.98),rgba(235,247,255,.98));
1680  border:2px solid #78c7f0; box-shadow:0 14px 38px rgba(0,30,66,.25);
1681}
1682.rosgan-account-assistant-head{display:flex;align-items:flex-start;gap:10px}
1683.rosgan-account-assistant-avatar{width:42px;height:42px;flex:0 0 42px;border-radius:50%;display:grid;place-items:center;background:#0673b8;color:#fff;font-size:21px;box-shadow:0 5px 14px rgba(6,115,184,.3)}
1684.rosgan-account-assistant-copy{min-width:0;flex:1}
1685.rosgan-account-assistant-copy strong{display:block;font-size:16px;line-height:1.15;color:#002e5f}
1686.rosgan-account-assistant-copy span{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:#496a88}
1687.rosgan-account-assistant-close{width:32px;height:32px;flex:0 0 32px;border:0;border-radius:50%;background:#e4f1fa;color:#164c77;font:900 18px/1 inherit;cursor:pointer}
1688.rosgan-account-assistant-text{margin:0;font-size:14px;line-height:1.52;color:#163f62}
1689.rosgan-account-assistant-tip{padding:10px 11px;border-radius:11px;background:#e8f7ef;border:1px solid #a9dfc3;color:#12623d;font-size:12.5px;line-height:1.45;font-weight:700}
1690.rosgan-account-assistant-error{padding:9px 10px;border-radius:10px;background:#fff0f1;border:1px solid #f2b8bd;color:#a51f2a;font-size:12.5px;line-height:1.4;font-weight:750}
1691.rosgan-account-assistant-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px}
1692.rosgan-account-assistant-actions button{min-height:44px;border-radius:11px;padding:10px 12px;font:800 13px/1.25 inherit;cursor:pointer}
1693.rosgan-account-assistant-primary{border:0;background:#006cb4;color:#fff}
1694.rosgan-account-assistant-secondary{border:1px solid #b7d4e9;background:#fff;color:#174d76}
1695.rosgan-account-assistant-progress{display:flex;align-items:center;gap:7px;color:#50708b;font-size:11px;font-weight:800}
1696.rosgan-account-assistant-progress span{height:7px;flex:1;border-radius:999px;background:#cfdeeb;overflow:hidden}
1697.rosgan-account-assistant-progress i{display:block;height:100%;border-radius:inherit;background:#0794d2;width:var(--account-help-progress,0%);transition:width 220ms ease}
1698.rosgan-auth-field-assisted{border-color:#65d6ff!important;box-shadow:0 0 0 4px rgba(55,193,246,.24),0 0 22px rgba(55,193,246,.2)!important}
1699.rosgan-account-verification-help{padding:13px 14px;border-radius:14px;background:#eaf7ff;border:2px solid #7dc8ee;color:#123f62;display:grid;gap:6px}
1700.rosgan-account-verification-help strong{font-size:15px;color:#002e5f}.rosgan-account-verification-help span{font-size:12.5px;line-height:1.48}
1701@media(max-width:520px){
1702  .rosgan-account-assistant{padding:13px;gap:10px}
1703  .rosgan-account-assistant-actions{grid-template-columns:1fr}
1704  .rosgan-account-assistant-actions button{min-height:46px;font-size:13.5px}
1705}
1706@supports (-webkit-touch-callout:none){
1707  .rosgan-auth-card{
1708    background:rgba(0,29,61,.9)!important;
1709    -webkit-backdrop-filter:none!important;
1710    backdrop-filter:none!important;
1711  }
1712}
1713input, select, textarea, button { box-sizing:border-box; max-width:100%; }
1714a { color:#0a5ca8; text-decoration:none; } a:hover { color:#002e5f; }
1715@keyframes kenburns { from { transform:scale(1.03); } to { transform:scale(1.12); } }
1716@keyframes pulseDot { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
1717@keyframes badgePop { from { opacity:0; transform:scale(0.6); } to { opacity:1; transform:scale(1); } }
1718@keyframes pageIn { from { opacity:0; transform:translateY(18px); } to { opacity:1; transform:translateY(0); } }
1719@keyframes prebidSuccessFlood {
1720  0% { opacity:.15; transform:scale(.015); }
1721  18% { opacity:1; }
1722  100% { opacity:1; transform:scale(1); }
1723}
1724@keyframes prebidSuccessCheck {
1725  0%,32% { opacity:0; transform:scale(.35) rotate(-18deg); }
1726  62% { opacity:1; transform:scale(1.16) rotate(4deg); }
1727  78% { transform:scale(.94) rotate(-1deg); }
1728  100% { opacity:1; transform:scale(1) rotate(0); }
1729}
1730@keyframes prebidSuccessCopy {
1731  0%,42% { opacity:0; transform:translateY(18px); }
1732  100% { opacity:1; transform:translateY(0); }
1733}
1734@keyframes prebidSuccessLayerOut {
1735  0%,82% { opacity:1; }
1736  100% { opacity:0; visibility:hidden; }
1737}
1738.prebid-success-layer {
1739  position:fixed;
1740  inset:0;
1741  z-index:110000;
1742  display:grid;
1743  place-items:center;
1744  overflow:hidden;
1745  padding:24px;
1746  color:#fff;
1747  text-align:center;
1748  isolation:isolate;
1749  pointer-events:auto;
1750  animation:prebidSuccessLayerOut 1850ms ease-in both;
1751}
1752.prebid-success-flood {
1753  position:absolute;
1754  z-index:-1;
1755  width:180vmax;
1756  height:180vmax;
1757  border-radius:50%;
1758  background:radial-gradient(circle at 50% 42%,rgba(71,221,165,.95) 0,rgba(24,173,117,.98) 30%,#087653 72%,#045b40 100%);
1759  box-shadow:0 0 0 100vmax #045b40;
1760  transform:scale(.015);
1761  animation:prebidSuccessFlood 660ms cubic-bezier(.18,.82,.24,1) both;
1762}
1763.prebid-success-content { display:grid; justify-items:center; width:min(520px,100%); }
1764.prebid-success-check {
1765  display:grid;
1766  place-items:center;
1767  width:96px;
1768  height:96px;
1769  border-radius:50%;
1770  background:#fff;
1771  box-shadow:0 24px 55px rgba(0,57,39,.28),0 0 0 12px rgba(255,255,255,.13);
1772  animation:prebidSuccessCheck 880ms cubic-bezier(.16,1,.3,1) both;
1773}
1774.prebid-success-check img { width:50px; height:50px; filter:invert(34%) sepia(72%) saturate(706%) hue-rotate(111deg) brightness(91%) contrast(92%); }
1775.prebid-success-copy { margin-top:26px; animation:prebidSuccessCopy 900ms cubic-bezier(.16,1,.3,1) both; }
1776.prebid-success-copy small { display:block; margin-bottom:8px; color:rgba(255,255,255,.78); font-size:11px; font-weight:900; letter-spacing:.16em; }
1777.prebid-success-copy h2 { margin:0; color:#fff; font-size:clamp(32px,6vw,52px); line-height:1; letter-spacing:-.035em; }
1778.prebid-success-amount { display:block; margin-top:14px; color:#fff; font-size:clamp(27px,5vw,42px); font-weight:950; font-variant-numeric:tabular-nums; }
1779.prebid-success-copy p { margin:10px 0 0; color:rgba(255,255,255,.86); font-size:15px; font-weight:750; }
1780@keyframes roslotCardBidSuccess {
1781  0% { opacity:0; transform:scale(.985); }
1782  12%,78% { opacity:1; transform:scale(1); }
1783  100% { opacity:0; transform:scale(1.01); }
1784}
1785.roslot-op-success {
1786  position:absolute;
1787  z-index:8;
1788  inset:0;
1789  display:flex;
1790  align-items:center;
1791  justify-content:center;
1792  gap:9px;
1793  overflow:hidden;
1794  border-radius:inherit;
1795  background:linear-gradient(105deg,#11966d,#087653);
1796  color:#fff;
1797  box-shadow:inset 0 0 0 1px rgba(255,255,255,.18);
1798  pointer-events:none;
1799  animation:roslotCardBidSuccess 1750ms ease-in-out both;
1800}
1801.roslot-op-success-lottie { display:block; width:48px; height:48px; flex:0 0 48px; }
1802.roslot-op-success-copy { display:grid; gap:1px; text-align:left; }
1803.roslot-op-success-copy small { color:rgba(255,255,255,.82); font-size:8px; font-weight:950; letter-spacing:.12em; }
1804.roslot-op-success-copy strong { color:#fff; font-size:17px; font-weight:950; line-height:1; font-variant-numeric:tabular-nums; }
1805.roslot-op-success-copy span { color:rgba(255,255,255,.88); font-size:9px; font-weight:750; }
1806.roslot-bid-card { position:relative; overflow:hidden; }
1807.roslot-op-success.is-detail { flex-direction:column; gap:0; }
1808.roslot-op-success.is-detail .roslot-op-success-lottie { width:96px; height:96px; flex-basis:96px; margin-bottom:-8px; }
1809.roslot-op-success.is-detail .roslot-op-success-copy { text-align:center; }
1810.roslot-op-success.is-detail .roslot-op-success-copy small { font-size:9px; }
1811.roslot-op-success.is-detail .roslot-op-success-copy strong { margin-top:5px; font-size:28px; }
1812.roslot-op-success.is-detail .roslot-op-success-copy span { margin-top:5px; font-size:11px; }
1813@media (prefers-reduced-motion:reduce) {
1814  .prebid-success-layer { animation-duration:1200ms; }
1815  .roslot-op-success { animation-duration:900ms; }
1816  .prebid-success-flood,.prebid-success-check,.prebid-success-copy { animation-duration:1ms; animation-delay:0ms; }
1817}
1818    @keyframes shimmer { 0% { background-position:-500px 0; } 100% { background-position:500px 0; } }
1819    @keyframes dc-spin { to { transform:rotate(360deg); } }
1820    .dc-skeleton {
1821      color:transparent !important;
1822      background:linear-gradient(100deg,#e7edf4 20%,#f8fafc 40%,#e7edf4 60%) !important;
1823      background-size:220% 100% !important;
1824      animation:shimmer 1.45s infinite !important;
1825      border-color:transparent !important;
1826      box-shadow:none !important;
1827      pointer-events:none;
1828      user-select:none;
1829    }
1830    .home-skeleton {
1831      position:relative;
1832      overflow:hidden;
1833      color:transparent !important;
1834      background:#e7edf4 !important;
1835      border-color:transparent !important;
1836      box-shadow:none !important;
1837      pointer-events:none;
1838      user-select:none;
1839    }
1840    .home-skeleton::after {
1841      content:"";
1842      position:absolute;
1843      inset:0;
1844      transform:translateX(-115%);
1845      background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,.84) 48%, transparent 100%);
1846      animation:homeSkeletonSweep 1.35s ease-in-out infinite;
1847    }
1848    .home-skeleton-dark { background:rgba(255,255,255,.12) !important; }
1849    .home-skeleton-dark::after { background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,.22) 48%, transparent 100%); }
1850    .home-loader-card {
1851      min-width:0;
1852      overflow:hidden;
1853      border:1px solid #e5ecf3;
1854      border-radius:16px;
1855      background:#fff;
1856      box-shadow:0 8px 26px rgba(0,30,60,.07);
1857    }
1858    @keyframes homeSkeletonSweep { to { transform:translateX(115%); } }
1859    @media (prefers-reduced-motion:reduce) {
1860      .home-skeleton::after { animation:none; display:none; }
1861    }
1862    .dc-media-loader {
1863      position:absolute;
1864      inset:0;
1865      z-index:3;
1866      display:grid;
1867      place-items:center;
1868      background:linear-gradient(100deg,#dfe7f0 20%,#f6f9fc 40%,#dfe7f0 60%);
1869      background-size:220% 100%;
1870      animation:shimmer 1.45s infinite;
1871    }
1872    .dc-spinner {
1873      width:34px;
1874      height:34px;
1875      border:3px solid rgba(10,92,168,.2);
1876      border-top-color:#0a5ca8;
1877      border-radius:50%;
1878      animation:dc-spin .8s linear infinite;
1879    }
1880    .dc-media-image {
1881      background:linear-gradient(100deg,#e7edf4 20%,#f8fafc 40%,#e7edf4 60%);
1882      background-size:220% 100%;
1883      animation:shimmer 1.45s infinite;
1884      color:transparent;
1885    }
1886    .dc-media-image[data-dc-loaded="true"] {
1887      background:none;
1888      animation:none;
1889    }
1890    .dc-cabana-card-media {
1891      transition:opacity .32s ease;
1892      will-change:opacity;
1893    }
1894    .cabana-auction-card { display:flex; flex-direction:column; min-width:0; overflow:hidden; border:1px solid #dce6f0; border-radius:20px; background:#fff; box-shadow:0 12px 34px rgba(0,29,61,.08); transition:transform 180ms ease,box-shadow 180ms ease,border-color 180ms ease; }
1895    .cabana-auction-card:hover { transform:translateY(-4px); border-color:#c5d6e7; box-shadow:0 22px 48px rgba(0,29,61,.14); }
1896    .cabana-card-hero { position:relative; min-height:176px; overflow:hidden; isolation:isolate; background:#eaf0f6; }
1897    .cabana-card-hero-media-stack { position:absolute; inset:0; z-index:0; overflow:hidden; background:transparent; }
1898    .cabana-card-hero-media-base { position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; }
1899    .cabana-card-hero-media { position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; opacity:0; transform:scale(1.015); animation:cabanaCardMediaCycle 40s linear infinite; }
1900    .cabana-card-hero-shade { position:absolute; inset:0; z-index:1; background:linear-gradient(180deg,rgba(0,24,49,.42) 0%,rgba(0,24,49,.04) 42%,rgba(0,24,49,.9) 100%); pointer-events:none; }
1901    .cabana-card-hero-fallback { position:absolute; inset:0; z-index:0; display:grid; place-items:center; padding:28px; box-sizing:border-box; background:#eef3f8; }
1902    .cabana-card-hero-fallback img { display:block; width:auto; max-width:66%; max-height:72px; object-fit:contain; }
1903
1904.home-channel{padding:28px clamp(16px,4vw,36px) 4px;color:#112033}.home-channel>header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.home-channel>header small{font-size:11px;letter-spacing:.14em;font-weight:800;color:#075da8}.home-channel h2{font-size:clamp(21px,2.5vw,28px);margin:4px 0}.home-channel>header button{min-height:44px;border:1px solid #cfdeeb;border-radius:12px;background:#fff;color:#075da8;padding:10px 14px;cursor:pointer}.home-channel-layout{display:grid;grid-template-columns:minmax(0,2.2fr) minmax(250px,1fr);border-radius:20px;overflow:hidden;background:#062541;box-shadow:0 12px 30px #002e5f12}.home-channel-screen{aspect-ratio:16/9;background:#00162a;align-self:start;scroll-margin-top:80px}.home-channel-screen video{display:block;width:100%;height:100%;object-fit:contain}.home-channel-screen iframe{display:block;width:100%;height:100%;border:0}.home-channel-empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#fff;padding:24px;box-sizing:border-box}.home-channel-empty img{width:160px;filter:brightness(0) invert(1);margin-bottom:15px}.home-channel-empty h3{margin:8px 0;font-size:22px}.home-channel-empty p{color:#b7cbdd;font-size:14px}.home-channel-layout aside{color:#fff;min-width:0}.home-channel-current{padding:24px}.home-channel-current h3{font-size:clamp(18px,2vw,25px);line-height:1.2;margin:14px 0 10px}.home-channel-current p{font-size:13px;color:#b7cbdd}.home-channel-current>small{display:block;font-size:12px;line-height:1.5;color:#b7cbdd}.home-channel-current a{display:inline-block;color:#fff;font-size:13px;margin-top:16px}.home-channel-badge{font-size:10px;font-weight:850;letter-spacing:.08em;background:#ffffff1c;padding:6px 10px;border-radius:20px}.home-channel-badge.is-live{background:#e63946}.home-channel-today{border-top:1px solid #ffffff1c;padding:16px 24px;max-height:220px;overflow-y:auto}.home-channel-today h4{margin:0 0 12px;font-size:13px}.home-channel-today button{display:flex;text-align:left;gap:10px;width:100%;padding:9px;border:1px solid transparent;border-radius:10px;background:transparent;color:#fff;cursor:pointer}.home-channel-today button[aria-pressed="true"]{background:#ffffff12;border-color:#74b6ea}.home-channel-today img{width:76px;aspect-ratio:16/9;object-fit:cover;border-radius:6px;align-self:center}.home-channel-today strong{font-size:12px;line-height:1.4;display:block}.home-channel-today small{display:block;font-size:11px;color:#b7cbdd;margin-top:5px}.home-channel button:focus-visible,.home-channel a:focus-visible{outline:3px solid #74b6ea;outline-offset:2px}@media(max-width:800px){.home-channel-layout{grid-template-columns:1fr;border-radius:16px}.home-channel-current{padding:18px}.home-channel-current h3{font-size:21px}.home-channel-today{padding:16px 18px}.home-channel>header{align-items:flex-start}.home-channel>header button{font-size:11px;max-width:160px}.home-channel-empty h3{font-size:18px}.home-channel-empty img{width:110px}.home-channel-empty p{font-size:12px}}
1905
1906.home-channel-layout{align-items:start}.home-channel-layout aside{max-height:100%;overflow:hidden}.home-channel-links{display:flex;align-items:center;gap:16px;margin-top:14px}.home-channel-links a{margin:0}.home-channel-links button{border:0;padding:6px 0;background:transparent;
1906color:#bdddf6;font-size:12px;cursor:pointer}.home-channel-today{box-sizing:border-box;max-height:190px}.home-channel-today summary{font-size:13px;font-weight:750;cursor:pointer;padding:2px 0 10px}.home-channel-programs{display:grid;gap:3px}
1907@media(min-width:801px){.home-channel-layout{position:relative}.home-channel-layout aside{position:absolute;right:0;top:0;bottom:0;width:31.25%;display:flex;flex-direction:column}.home-channel-current{padding:18px 22px;flex:none}.home-channel-current h3{font-size:21px}.home-channel-today{min-height:0;flex:1;max-height:none}}
1908@media(max-width:800px){.home-channel{padding-top:20px}.home-channel>header{margin-bottom:10px}.home-channel h2{font-size:20px}.home-channel-current{padding:12px 14px}.home-channel-current h3{font-size:16px;line-height:1.25;margin:8px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.home-channel-current>p,.home-channel-current>small{display:none}.home-channel-links{margin-top:6px;font-size:12px}.home-channel-badge{font-size:9px;padding:4px 8px}.home-channel-today{padding:10px 14px;max-height:180px}.home-channel-today summary{padding:0;min-height:24px}.home-channel-today[open] summary{margin-bottom:8px}.home-channel-today strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.home-channel>header button{min-height:36px;padding:6px 10px}}
1909
1910.home-channel-picker{display:flex;flex-direction:column;min-height:0;overflow:hidden;padding:12px 14px!important}.home-channel-picker-heading{flex:none;margin-bottom:9px}.home-channel-picker-heading>strong{font-size:11px;color:#bcd3e7}.home-channel-programs{display:flex;flex-direction:column;gap:8px;min-height:0;overflow-x:hidden;overflow-y:auto;padding:2px;scrollbar-width:thin;scrollbar-color:#6689a3 transparent;overscroll-behavior-y:contain}.home-channel-programs .home-channel-chip{box-sizing:border-box;flex:none;width:100%;min-width:0;min-height:64px;padding:10px;border:1px solid #ffffff26;border-radius:11px;background:#ffffff09;align-items:center;gap:11px}.home-channel-programs .home-channel-chip[aria-pressed="true"]{background:#155b8a;border-color:#89caff;box-shadow:inset 3px 0 0 #89caff}.home-channel-chip img{width:64px;height:40px;border-radius:7px;object-fit:cover;flex:none}.home-channel-chip>span{min-width:0;flex:1}.home-channel-chip strong{font-size:12px;line-height:1.4;display:block;overflow-wrap:anywhere}.home-channel-chip small{font-size:11px;margin-top:4px}.home-channel-chip:focus-visible{outline-offset:-2px!important}@media(max-width:800px){.home-channel-picker{padding:12px 14px!important;max-height:260px}.home-channel-chip strong{display:block;-webkit-line-clamp:unset}.home-channel-chip img{width:64px;height:40px}}
1911.home-channel-links .home-channel-youtube{display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:42px;padding:0 12px;border:1px solid #ffffff32;border-radius:10px;background:#ffffff0d;color:#fff;font-size:12px;font-weight:700;text-decoration:none;transition:background .15s,border-color .15s}.home-channel-links .home-channel-youtube:hover{background:#ffffff1c;border-color:#ffffff65}.home-channel-youtube svg{flex:none}@media(max-width:800px){.home-channel-links .home-channel-youtube{min-height:40px;font-size:11px;gap:8px}}
1912    .cabana-card-hero-fallback.is-loading img { animation:cabanaCardLogoBreathe 1.5s ease-in-out infinite; }
1913    .cabana-card-hero-open { position:absolute; inset:0; z-index:1; width:100%; height:100%; border:0; padding:0; background:transparent; cursor:pointer; }
1914    .cabana-card-hero-content { position:relative; z-index:2; min-height:176px; box-sizing:border-box; display:flex; flex-direction:column; align-items:flex-start; padding:15px 16px 16px; color:#fff; pointer-events:none; }
1915    .cabana-card-topline { width:100%; display:flex; align-items:center; justify-content:flex-start; gap:9px; }
1916    .cabana-card-status { display:inline-flex; align-items:center; gap:6px; padding:5px 9px; border-radius:100px; color:#fff; font-size:9px; font-weight:900; letter-spacing:.08em; }
1917    .cabana-card-status::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; opacity:.9; }
1918    .cabana-card-breed { overflow:hidden; max-width:52%; padding:5px 9px; border:1px solid rgba(255,255,255,.24); border-radius:100px; background:rgba(255,255,255,.12); color:#fff; font-size:9px; font-weight:800; text-overflow:ellipsis; white-space:nowrap; backdrop-filter:blur(8px); }
1919    .cabana-card-title { display:-webkit-box; overflow:hidden; max-width:100%; margin:auto 0 0; color:#fff; font-size:20px; line-height:1.13; letter-spacing:-.025em; text-shadow:0 2px 14px rgba(0,20,42,.4); -webkit-line-clamp:2; -webkit-box-orient:vertical; }
1920    @keyframes cabanaCardMediaCycle { 0%,10% { opacity:1; transform:scale(1.015); } 12.5%,97% { opacity:0; transform:scale(1.055); } 100% { opacity:1; transform:scale(1.015); } }
1921    @keyframes cabanaCardLogoBreathe { 0%,100% { opacity:.62; transform:scale(.985); } 50% { opacity:1; transform:scale(1); } }
1922    @media(prefers-reduced-motion:reduce){.cabana-card-hero-media{display:none;animation:none}.cabana-card-hero-media:first-child{display:block;opacity:1;transform:none}.cabana-card-hero-fallback.is-loading img{animation:none}}
1923    .cabana-card-body { display:grid; gap:11px; padding:15px 17px 17px; }
1924    .cabana-card-meta { display:flex; align-items:flex-start; gap:12px; min-width:0; padding-bottom:11px; border-bottom:1px solid #edf1f5; }
1925    .cabana-card-meta-item { min-width:0; flex:1 1 0; display:grid; grid-template-columns:17px minmax(0,1fr); gap:7px; align-items:start; padding:0; }
1926    .cabana-card-meta-item + .cabana-card-meta-item { padding-left:12px; border-left:1px solid #e4eaf0; }
1927    .cabana-card-meta-icon { width:17px; height:17px; display:grid; place-items:center; margin-top:1px; }
1928    .cabana-card-meta-icon img { width:13px; height:13px; opacity:.68; }
1929    .cabana-card-meta-item small { display:block; color:#8492a5; font-size:8px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
1930    .cabana-card-meta-item strong { display:block; overflow:visible; margin-top:2px; color:#19334f; font-size:11px; line-height:1.3; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; }
1931    .cabana-card-location strong { overflow:visible; text-overflow:clip; white-space:normal; }
1932    .cabana-card-brand-strip { min-width:0; display:flex; align-items:center; gap:12px; min-height:54px; padding:8px 11px; border:1px solid #e7edf4; border-radius:13px; background:#fff; }
1933    .cabana-card-brand-strip{align-items:flex-start;flex-direction:column;gap:8px}
1934    .cabana-card-participants{display:flex;flex-wrap:wrap;align-items:center;gap:8px;width:100%;min-width:0}
1935    .cabana-card-participant{display:flex;align-items:center;justify-content:center;min-width:0;width:52px;min-height:44px;box-sizing:border-box;padding:4px;border-radius:8px}
1936
1937    .cabana-participants-window{width:100%;min-width:0}
1938    .cabana-participants-track{min-width:0}
1939    .cabana-card-brand-strip.is-rolling .cabana-card-brand-label{display:flex;align-items:center;justify-content:space-between;gap:8px}
1940    .cabana-roll-toggle{border:0;background:transparent;color:#0a5ca8;font:600 10px inherit;cursor:pointer;padding:3px 5px}
1941    .cabana-card-brand-strip.is-rolling .cabana-participants-window{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;overscroll-behavior-x:contain}
1942    .cabana-card-brand-strip.is-rolling .cabana-participants-track{display:flex;width:max-content;animation:cabanaParticipantsRoll var(--participants-duration,40s) linear infinite}
1943    .cabana-card-brand-strip.is-rolling .cabana-card-participants{flex:none;flex-wrap:nowrap;width:auto;gap:8px;padding-right:8px}
1944    .cabana-card-brand-strip.is-rolling .cabana-card-participant{flex:0 0 100px;width:100px;height:56px}
1945    .cabana-card-brand-strip.is-rolling:hover .cabana-participants-track,.cabana-card-brand-strip.is-rolling:focus-within .cabana-p
1945articipants-track,.cabana-card-brand-strip.is-paused .cabana-participants-track{animation-play-state:paused}
1946    @keyframes cabanaParticipantsRoll{to{transform:translateX(-50%)}}
1947    @media(prefers-reduced-motion:reduce){.cabana-card-brand-strip.is-rolling .cabana-participants-track{animation:none}.cabana-participants-copy,.cabana-roll-toggle{display:none}}
1948    .cabana-card-participant.is-principal{width:100px}
1949    .cabana-card-participant img{display:block;max-width:100%;width:auto;height:36px;object-fit:contain}
1950    .cabana-card-participant>span{font-size:10px;line-height:1.25;color:#516982;overflow-wrap:anywhere;text-align:center}
1951    .cabana-card-brand-label { flex:0 0 auto; color:#8492a5; font-size:8px; font-weight:900; letter-spacing:.09em; text-transform:uppercase; }
1952    .cabana-card-main-logo { min-width:0; height:38px; flex:1 1 auto; display:flex; align-items:center; padding:3px 5px; border-radius:8px; box-sizing:border-box; transition:background-color .18s ease; }
1953    .cabana-card-main-logo img { max-width:100%; max-height:32px; object-fit:contain; object-position:left center; }
1954    .cabana-card-guest-logos { flex:0 1 auto; display:flex; align-items:center; gap:8px; overflow:visible; }
1955    .cabana-card-guest-logo { width:46px; height:38px; display:grid; place-items:center; padding:4px; border-radius:8px; box-sizing:border-box; overflow:visible; transition:background-color .18s ease; }
1956    .cabana-card-guest-logo img { display:block; width:auto; height:auto; max-width:100%; max-height:100%; object-fit:contain; }
1957    .cabana-card-main-logo.is-light-logo img,.cabana-card-guest-logo.is-light-logo img { filter:brightness(0) saturate(100%); }
1958    .cabana-card-consignor { min-width:0; display:flex; align-items:center; gap:9px; padding-top:1px; }
1959    .cabana-card-consignor-icon { flex:none; width:29px; height:29px; display:grid; place-items:center; border-radius:9px; }
1960    .cabana-card-consignor-icon img { width:17px; height:17px; object-fit:contain; }
1961    .cabana-card-consignor-copy { min-width:0; }
1962    .cabana-card-consignor-copy small { display:block; color:#8492a5; font-size:8px; font-weight:900; letter-spacing:.09em; text-transform:uppercase; }
1963    .cabana-card-consignor-copy strong { display:block; overflow:hidden; margin-top:2px; color:#19334f; font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
1964    .cabana-card-admin-actions { display:flex; gap:7px; flex-wrap:wrap; }
1965    .cabana-card-admin-action { flex:1 1 90px; min-width:0; display:flex; align-items:center; justify-content:center; gap:6px; border:1px solid #d6e1ed; border-radius:10px; padding:8px 9px; background:#fff; color:#496077; font:800 10px inherit; cursor:pointer; }
1966    .cabana-card-admin-action img { width:13px; height:13px; opacity:.75; }
1967    @media (max-width:960px) {
1968      .cabana-auction-grid { grid-template-columns:minmax(0,1fr) !important; }
1969    }
1970    @media (max-width:520px) {
1971      .cabana-card-hero,.cabana-card-hero-content { min-height:164px; }
1972      .cabana-card-title { font-size:18px; }
1973      .cabana-card-meta { gap:9px; }
1974      .cabana-card-meta-item + .cabana-card-meta-item { padding-left:9px; }
1975      .cabana-card-brand-strip { align-items:flex-start; flex-wrap:wrap; }
1976      .cabana-card-brand-label { width:100%; }
1977    }
1978    @keyframes loadSlide { 0% { transform:translateX(-120%); } 100% { transform:translateX(320%); } }
1979@keyframes floatIn { from { opacity:0; transform:translateY(26px); } to { opacity:1; transform:translateY(0); } }
1980@keyframes bidPulse { 0% { box-shadow:0 0 0 0 rgba(230,57,70,0.45); } 100% { box-shadow:0 0 0 22px rgba(230,57,70,0); } }
1981@keyframes livePing { 0% { transform:scale(1); opacity:0.7; } 100% { transform:scale(2.4); opacity:0; } }
1982@keyframes drawLine { to { stroke-dashoffset:0; } }
1983@keyframes growBar { to { transform:scaleX(1); } }
1984@keyframes fadeScale { from { opacity:0; transform:scale(0.9); } to { opacity:1; transform:scale(1); } }
1985@keyframes spinIn { from { opacity:0; transform:rotate(-25deg) scale(0.85); } to { opacity:1; transform:rotate(0) scale(1); } }
1986@keyframes spin { to { transform:rotate(360deg); } }
1987@keyframes gradientShift { 0% { background-position:0% 50%; } 50% { background-position:100% 50%; } 100% { background-position:0% 50%; } }
1988@keyframes gradientBlob { 0% { transform:translate(-8%,-6%) scale(1); } 33% { transform:translate(10%,4%) scale(1.15); } 66% { transform:translate(-4%,8%) scale(1.05); } 100% { transform:translate(-8%,-6%) scale(1); } }
1989@keyframes logoPop { 0% { opacity:0; transform:scale(0.4) rotate(-8deg); } 55% { opacity:1; transform:scale(1.12) rotate(2deg); } 75% { transform:scale(0.97) rotate(0deg); } 100% { opacity:1; transform:scale(1) rotate(0deg); } }
1990@keyframes logoBreathe { 0%,100% { transform:scale(1); box-shadow:0 12px 34px rgba(0,0,0,0.22), 0 0 0 0 rgba(255,255,255,0.55); } 50% { transform:scale(1.06); box-shadow:0 20px 48px rgba(0,0,0,0.3), 0 0 0 14px rgba(255,255,255,0); } }
1991@keyframes logoWrap { 0% { opacity:0; transform:scale(0.55); } 60% { opacity:1; transform:scale(1.08); } 100% { opacity:1; transform:scale(1); } }
1992@keyframes initialPop { 0% { opacity:0; transform:scale(0.4); text-shadow:0 0 0 rgba(255,255,255,0); } 60% { opacity:1; transform:scale(1.18); } 100% { opacity:1; transform:scale(1); } }
1993@keyframes initialGlow { 0%,100% { text-shadow:0 0 12px rgba(255,255,255,0.25); transform:scale(1); } 50% { text-shadow:0 0 34px rgba(255,255,255,0.75), 0 0 60px rgba(255,255,255,0.35); transform:scale(1.08); } }
1994@keyframes heroTextFade { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
1995@keyframes heroImageReveal { from { opacity:.55; filter:saturate(.72); } to { opacity:1; filter:saturate(1); } }
1996@media(max-width:700px){
1997  .home-hero{width:100%;aspect-ratio:16/9;height:auto!important;min-height:0!important;padding-bottom:0!important;overflow:hidden}
1998  .home-hero-content{height:100%;min-height:0;overflow:hidden}
1999  .home-hero-nav{padding:8px 18px!important;flex:0 0 auto}
2000  .home-hero-main{min-height:0;padding:7px 18px 10px!important;display:flex!important;flex-direction:column;align-items:stretch!important;justify-content:flex-end!important;gap:12px!important;flex-wrap:nowrap!important;overflow:hidden}
2001  .home-hero-copy{width:100%!important;max-width:none!important;flex:0 1 auto!important;min-height:0}
2002  .home-hero-brand{width:min(124px,37vw)!important;height:20px!important;margin-bottom:7px!important}
2003  .home-hero-badge{width:max-content;max-width:100%;gap:4px!important;padding:3px 8px!important;font-size:7.5px!important;line-height:1!important;white-space:nowrap!important;flex-wrap:nowrap!important}
2004  .home-hero-badge>.home-hero-status-dot{width:4px!important;height:4px!important}
2005  .home-hero-title{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden!important;margin:9px 0 7px!important;max-width:92%!important;font-size:clamp(16px,4.15vw,19px)!important;line-height:1.14!important;letter-spacing:-.018em!important}
2006  .home-hero-subtitle{min-height:0!important;max-width:92%;font-size:8px!important;line-height:1.3!important;white-space:nowrap;text-overflow:ellipsis;-webkit-line-clamp:1!important}
2007  .home-hero-actions{gap:7px!important;margin-top:10px!important;flex-wrap:nowrap!important}
2008  .home-hero-actions>a,.home-hero-actions>span{padding:5px 8px!important;border-radius:7px!important;font-size:8px!important;white-space:nowrap}
2009  .home-hero-dots{gap:5px!important;margin-top:7px!important}
2010  .home-hero-dots>span{height:3px!important}
2011  .home-hero-indices{width:100%;display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px!important;flex:0 0 auto}
2012  .home-hero-index-card{min-width:0!important;border-radius:8px!important;padding:5px 7px!important}
2013  .home-hero-index-card>div:first-child{gap:3px!important}
2014  .home-hero-index-card>div:first-child>
2014span:first-child{font-size:7px!important;letter-spacing:.04em!important}
2015  .home-hero-index-card>div:first-child>span:last-child{display:none!important}
2016  .home-hero-index-value{margin-top:3px!important;font-size:clamp(11px,3vw,14px)!important;line-height:1!important;white-space:nowrap}
2017  .home-hero-index-value>.home-hero-index-unit{font-size:6px!important}
2018  .home-hero-index-month{display:none!important}
2019  .home-month-card-media{height:180px!important}
2020  .home-month-card-title{font-size:13.5px!important;line-height:1.14!important}
2021  .home-month-card-meta{font-size:9.2px!important;line-height:1.2!important}
2022}
2023@keyframes drawLine { from { stroke-dashoffset:1; } to { stroke-dashoffset:0; } }
2024@keyframes areaRise { from { opacity:0; } to { opacity:1; } }
2025.sales-order-page{min-height:calc(100vh - 68px);box-sizing:border-box;overflow-x:hidden;background:#f2f6fb;padding:28px clamp(14px,4vw,40px) 110px;color:#112033}
2026.sales-order-shell{width:min(1180px,100%);margin:0 auto}
2027.sales-order-back{display:inline-flex;align-items:center;gap:7px;border:0;background:transparent;color:#0a5ca8;padding:0;cursor:pointer;font:800 13px inherit}
2028
2029.rg-order-save{flex-shrink:0}.rg-order-save .primary img{filter:brightness(0) invert(1)!important}.rg-order-save .sales-order-action.primary{background:#0a5ca8!important;color:#fff!important;border-color:#0a5ca8!important;min-height:44px;padding:11px 22px;border-radius:10px;box-shadow:0 4px 12px #0a5ca81f}
2030.rg-order-tools{display:grid;grid-template-columns:1.15fr 1fr 1fr;gap:12px;margin:0 0 20px;align-items:start}
2031.rg-order-tool-group{min-width:0;padding:18px;background:#fff;border:1px solid #dce6ef;border-radius:13px}
2032.rg-order-tool-group h2{font-size:13px;font-weight:800;color:#143550;margin:0 0 5px}
2033.rg-order-tool-group p{font-size:12px;line-height:1.5;color:#6a7c8f;margin:0 0 15px}
2034.rg-order-tool-buttons{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
2035.rg-order-tool-buttons .sales-order-action{min-height:40px;border-radius:9px;font-weight:650}
2036.rg-order-tool-buttons img,.rg-order-save img{width:16px;height:16px;flex-shrink:0}
2037.rg-order-tools .sales-order-action:disabled{opacity:.5;cursor:not-allowed}
2038.rg-order-catalog-settings{margin-top:12px;border-top:1px solid #e8eef4;padding-top:10px}
2039.rg-order-catalog-settings summary{display:flex;justify-content:space-between;list-style:none;cursor:pointer;color:#426079;font-size:12px;font-weight:700;padding:4px 0}
2040.rg-order-catalog-settings summary::-webkit-details-marker{display:none}.rg-order-catalog-settings[open] summary{margin-bottom:12px}.rg-order-catalog-settings[open] summary span{transform:rotate(180deg)}
2041.rg-order-tools :focus-visible{outline:3px solid #65a5dc;outline-offset:3px}
2042@media(max-width:1000px){.rg-order-tools{grid-template-columns:1fr 1fr}.rg-order-tools>sc-if{grid-column:1/-1}}
2043@media(max-width:640px){.rg-order-tools{grid-template-columns:1fr}.rg-order-tools>sc-if{grid-column:auto}.rg-order-save.sales-order-head-actions{display:flex;width:auto}.rg-order-tool-group{padding:15px}.rg-order-tool-buttons .sales-order-action{width:auto;flex:1 1 auto}.rg-order-save .primary{width:auto}}
2044
2045.sales-order-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin:22px 0 16px}
2046.sales-order-head h1{margin:3px 0 5px;font-size:clamp(28px,4vw,42px);line-height:1.05}
2047.sales-order-head p{margin:0;color:#65758a;font-size:14px}
2048.sales-order-head-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}.sales-order-import{position:relative;overflow:hidden}.sales-order-import input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}.sales-order-import.is-busy{opacity:.62;pointer-events:none}
2049.sales-order-count{flex:0 0 auto;padding:7px 11px;border-radius:100px;background:#e6eef7;color:#34506d;font-size:12px;font-weight:900}
2050.sales-order-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:14px;padding:13px 14px;border:1px solid #d8e2ed;border-radius:8px;background:#fff}
2051.sales-order-toolbar-copy{display:grid;gap:2px;min-width:220px;flex:1}
2052.sales-order-toolbar-copy strong{font-size:13px}.sales-order-toolbar-copy span{color:#65758a;font-size:11px}
2053.sales-order-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
2054.sales-order-action{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:38px;border:1px solid #cbd8e7;border-radius:7px;padding:8px 12px;background:#fff;color:#33475b;cursor:pointer;font:800 12px inherit}
2055.sales-order-action.primary{border-color:transparent;background:var(--sales-accent,#0a5ca8);color:#fff}
2056.sales-order-message{margin:0 0 12px;padding:10px 12px;border-radius:7px;font-size:12px;font-weight:700}
2057.sales-order-message.error,.sales-order-message.is-error{background:#fff0f1;color:#b42332}.sales-order-message.success,.sales-order-message.is-success{background:#eaf8f2;color:#177458}
2058.sales-order-import-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 12px;padding:11px 13px;border:1px solid #bfd8ec;border-radius:10px;background:#f3f9ff;color:#174c78}.sales-order-import-summary strong{margin-right:auto;font-size:12px}.sales-order-import-summary span{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:100px;background:#fff;font-size:10.5px;font-weight:850}.sales-order-import-summary small{color:#607a93;font-size:10px}
2059.sales-order-filterbar{display:grid;grid-template-columns:minmax(220px,1fr) repeat(4,minmax(145px,.34fr));gap:9px;align-items:center;margin:0 0 14px;padding:11px;border:1px solid #d8e4f0;border-radius:12px;background:#fff}.sales-order-search{display:flex;align-items:center;gap:8px;min-width:0;height:40px;padding:0 11px;border:1px solid #cbd8e7;border-radius:9px;background:#f9fbfd}.sales-order-search img{width:17px;height:17px;opacity:.58}.sales-order-search input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#173a5d;font:700 12px inherit}.sales-order-filterbar select{min-width:0;width:100%;height:40px;padding:0 9px;border:1px solid #cbd8e7;border-radius:9px;background:#fff;color:#173a5d;font:750 11px inherit}.sales-order-filter-count{grid-column:1/-1;color:#60778e;font-size:10.5px}.sales-order-filter-count strong{color:#173a5d}
2060.sales-order-progress{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;font-size:12px}.sales-order-pending{padding:5px 9px;border-radius:7px;background:#fff3d9;color:#885807}.sales-order-pending strong{color:inherit}
2061.sales-order-kicker{display:block;color:#0a5ca8;font-size:10px;font-weight:900;letter-spacing:1.2px}
2062.sales-order-summary{margin:0 0 12px;color:#65758a;font-size:12px}.sales-order-summary strong{color:#173a5d}
2063.sales-order-copy{min-width:0}.sales-order-copy strong,.sales-order-copy small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sales-order-copy strong{font-size:14px}.sales-order-copy small{margin-top:3px;color:#65758a;font-size:11px}
2064.sales-order-empty{padding:40px 16px;border:1px dashed #cbd8e7;border-radius:8px;background:#fff;text-align:center;color:#65758a;font-size:13px}
2065.sales-order-list,.sales-order-groups{display:grid;gap:10px}
2066.sales-order-row{display:grid;grid-template-columns:35px minmax(0,1fr) auto;align-items:start;gap:12px;min-height:118px;padding:14px;border:1px solid #dbe5ef;border-radius:12px;background:#fff;box-shadow:0 5px 16px rgba(15,42,70,.045)}
2067.sales-order-lot-main{min-width:0;display:grid;gap:10px}.sales-order-facts{display:grid;grid-template-columns:repeat(4,minmax(90px,1fr));gap:7px}.sales-order-facts>span{min-width:0;padding:7px 9px;border-radius:8px;background:#f3f7fb}.sales-order-facts small,.sales-order-consignataria small{display:block;color:#7890a8;font-size:8.5px;font-weight:900;letter-spacing:.06em}.sales-order-facts strong{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#193a5b;font-size:11.5px}.sales-order-lot-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.sales-order-consignataria{min-width:210px;max-width:320px;display:flex;align-items:center;gap:9px}.sales-order-consignataria-logo{display:grid;place-items:center;width:36px;height:36px;box-sizing:border-box;padding:5px;border-radius:9px;flex:0 0 36px;overflow:hidden}.sales-order-consignataria-logo img{display:block;max-width:100%;max-height:100%;object-fit:contain}.sales-order-consignataria>span:last-child{min-width:0}.sales-order-consignataria strong{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#173a5d;font-size:11.5px}.sales-order-rating{display:flex;align-items:center;gap:7px;padding:5px 8px;border:1px solid #dce5ee;border-radius:8px;background:#fbfcfe}.sales-order-rating-label{min-width:67px;font-size:10px;font-weight:900}.sales-order-stars{display:flex;align-items:center;gap:1px}.sales-order-stars button{width:22px;height:22px;border:0;padding:0;background:transparent;cursor:pointer;font-size:19px;line-height:1;transition:transform .15s ease,color .15s ease}.sales-order-stars button:hover{transform:scale(1.2)}.sales-order-video-button{display:inline-flex;align-items:center;gap:7px;min-height:32px;padding:5px 9px;border:1px solid #bfd3e6;border-radius:8px;background:#f7fbff;color:#174c78;cursor:pointer;font:850 10.5px inherit}.sales-order-video-badge{display:inline-flex;align-items:center;border-radius:100px;padding:3px 6px;color:#fff;font-size:8px;font-weight:950;letter-spacing:.08em}.sales-order-video-badge.is-final{background:#16846e}.sales-order-video-badge.is-raw{background:#e07a19}.sales-order-no-video{color:#8a9aad;font-size:10.5px;font-weight:750}.sales-order-video-panel{grid-column:2/-1;display:grid;grid-template-columns:180px minmax(0,1fr);gap:14px;align-items:start;padding:12px;border-radius:10px;background:#071f38;color:#fff}.sales-order-video-panel>div{display:grid;gap:7px;justify-items:start}.sales-order-video-panel>div>strong{font-size:13px}.sales-order-video-panel>div>small{color:#d3deea;font-size:10.5px;line-height:1.4}.sales-order-video-direct{color:#fff;font-size:10.5px;font-weight:800;text-decoration:underline;text-underline-offset:3px}.sales-order-video-stage{position:relative;display:block;min-width:0;border-radius:8px;overflow:hidden;background:#001326}.sales-order-video-panel video{display:block;width:100%;max-height:390px;background:#001326}.sales-order-video-play{position:absolute;inset:50% auto auto 50%;transform:translate(-50%,-50%);display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 17px;border:1px solid rgba(255,255,255,.7);border-radius:100px;background:rgba(0,46,95,.9);box-shadow:0 8px 24px rgba(0,0,0,.32);color:#fff;cursor:pointer;font:900 12px inherit;transition:opacity .18s ease,transform .18s ease}.sales-order-video-play span{font-size:18px}.sales-order-video-play:hover{transform:translate(-50%,-50%) scale(1.04)}.sales-order-video-stage.is-playing .sales-order-video-play{opacity:0;pointer-events:none}.sales-order-video-stage.is-error .sales-order-video-play{display:none}
2068.sales-order-excel-data{display:grid;grid-template-columns:minmax(140px,1.5fr) repeat(2,minmax(90px,.55fr));gap:7px}.sales-order-excel-data>span{min-width:0;padding:7px 9px;border:1px solid #cfe0ef;border-radius:8px;background:#f7fbff}.sales-order-excel-data small{display:block;color:#6f89a1;font-size:8px;font-weight:900;letter-spacing:.07em}.sales-order-excel-data strong{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#0a5ca8;font-size:11px}
2069.sales-order-status{display:inline-flex;align-items:center;gap:5px;width:max-content;margin-top:5px;padding:4px 7px;border-radius:100px;font-size:8.5px;font-weight:950;letter-spacing:.06em;text-transform:uppercase}.sales-order-status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}.sales-order-status.is-cargado{background:#fff2d8;color:#855b14}.sales-order-status.is-controlado{background:#e7effe;color:#325aa6}.sales-order-status.is-publicado{background:#e2f3eb;color:#287054}.sales-order-status.is-anulado{background:#fbe8eb;color:#a3404e}.sales-order-status.is-otro{background:#edf1f5;color:#576a7c}
2070.sales-order-row-copy{min-width:0}.sales-order-row-copy strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}
2070.sales-order-row-copy span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:3px;color:#65758a;font-size:11px}
2071.sales-order-handle{width:38px;height:42px;display:grid;place-items:center;border:0;border-radius:7px;background:#edf3f9;color:#486683;cursor:grab;touch-action:none;pointer-events:none}.sales-order-handle:active{cursor:grabbing}.sales-order-handle img{width:19px;height:19px;pointer-events:none}
2072.sales-order-position{min-width:35px;height:30px;display:grid;place-items:center;border-radius:7px;background:var(--sales-accent,#0a5ca8);color:#fff;font-size:11px;font-weight:900}
2073.sales-order-category{overflow:hidden;border:1px solid #cad8e6;border-radius:8px;background:#fff}
2074.sales-order-category-head{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;padding:10px 12px;background:#e9f0f7}
2075.sales-order-category-head strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;color:#173a5d}.sales-order-category-head span{padding:5px 9px;border-radius:100px;background:#fff;color:#58718c;font-size:10px;font-weight:900}
2076.sales-order-category-list{display:grid;gap:7px;padding:9px}
2077.sales-order-controls{display:flex;align-items:center;justify-content:flex-end;gap:6px}
2078.sales-order-control{width:34px;height:34px;display:grid;place-items:center;border:1px solid #cbd8e7;border-radius:7px;background:#fff;cursor:pointer}.sales-order-control:disabled{opacity:.32;cursor:not-allowed}.sales-order-control img{width:17px;height:17px}
2079.sales-order-select{height:34px;min-width:58px;padding:0 7px;border:1px solid #cbd8e7;border-radius:7px;background:#fff;color:#173a5d;font:800 12px inherit}
2080.sales-order-drag-source{display:none!important}
2081.sales-order-drag-placeholder{box-sizing:border-box;border:2px dashed var(--sales-accent,#0a5ca8);border-radius:8px;background:rgba(10,92,168,.08)}
2082.sales-order-drag-proxy{position:fixed;z-index:10000;pointer-events:none;box-sizing:border-box;margin:0!important;opacity:.96;transform:rotate(.35deg);box-shadow:0 20px 50px rgba(15,42,70,.28)!important;will-change:left,top}
2083body.sales-order-is-dragging{cursor:grabbing!important;user-select:none!important;overscroll-behavior:none}
2084body.sales-order-is-dragging *{cursor:grabbing!important}
2085.sales-order-sortable-ghost{opacity:.22!important;background:#dceaf8!important}
2086.sales-order-sortable-chosen{box-shadow:0 14px 34px rgba(15,42,70,.2)!important}
2087.sales-order-sortable-drag{opacity:.98!important;cursor:grabbing!important}
2088.sales-order-native-drag{position:relative;z-index:5;opacity:.72;cursor:grabbing!important;box-shadow:0 16px 34px rgba(15,42,70,.2)!important;transform:scale(.995)}
2089.sales-order-native-target{outline:2px solid #1477c9;outline-offset:2px}
2090.sales-order-native-preview{position:fixed!important;inset:auto!important;z-index:9999!important;margin:0!important;pointer-events:none!important;opacity:.96!important;cursor:grabbing!important;box-shadow:0 20px 46px rgba(15,42,70,.28)!important;transform-origin:top left}
2091@media(max-width:1000px){.sales-order-filterbar{grid-template-columns:1fr 1fr}.sales-order-search{grid-column:1/-1}}
2092@media(max-width:680px){
2093  .sales-order-page{padding:18px 12px 96px}
2094  .sales-order-head{align-items:flex-start;margin-top:18px}.sales-order-head h1{font-size:29px}
2095  .sales-order-head-actions{width:100%;display:grid;grid-template-columns:1fr 1fr}.sales-order-head{flex-wrap:wrap}.sales-order-head-actions .sales-order-action{justify-content:center}
2096  .sales-order-filterbar{grid-template-columns:1fr 1fr}.sales-order-search{grid-column:1/-1}
2097  .sales-order-toolbar{align-items:stretch}.sales-order-actions{width:100%;display:grid;grid-template-columns:1fr 1fr}.sales-order-action{width:100%}
2098  .sales-order-row{grid-template-columns:32px minmax(0,1fr);padding:10px;gap:9px}.sales-order-row .sales-order-controls{grid-column:1/-1}.sales-order-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.sales-order-excel-data{grid-template-columns:1fr 1fr}.sales-order-excel-data>span:first-child{grid-column:1/-1}.sales-order-lot-tools{align-items:flex-start}.sales-order-rating{width:100%;box-sizing:border-box;justify-content:space-between}.sales-order-video-panel{grid-column:1/-1;grid-template-columns:1fr}.sales-order-video-panel video{max-height:280px}
2099  .sales-order-category-head{grid-template-columns:minmax(0,1fr);padding:9px;gap:9px}
2099.sales-order-category-head .sales-order-controls{justify-content:flex-start}
2100  .sales-order-row-copy strong{white-space:normal;line-height:1.25}
2101}
2102.commercial-page{min-height:100vh;background:#f3f7fb;color:#112033;padding:24px clamp(12px,3vw,32px) 96px}
2103.commercial-shell{width:min(1240px,100%);margin:0 auto}
2104.commercial-back{display:inline-flex;align-items:center;gap:8px;border:0;background:transparent;color:#0a5ca8;font:800 13px 'Archivo',sans-serif;padding:0;cursor:pointer}
2105.commercial-hero{margin-top:16px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:end}
2106.commercial-kicker{display:block;color:#0a5ca8;font-size:11px;font-weight:950;letter-spacing:.12em;text-transform:uppercase}
2107.commercial-hero h1{margin:5px 0 6px;font-size:clamp(29px,3.6vw,42px);line-height:1;color:#10233b;letter-spacing:0}
2108.commercial-hero p{margin:0;color:#65758a;font-size:14px;line-height:1.45}
2109.commercial-hero-actions{display:flex;gap:10px;align-items:center;justify-content:flex-end;flex-wrap:wrap}
2110.commercial-save,.commercial-prices-link{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:999px;background:#002e5f;color:#fff;padding:12px 16px;font:900 12px 'Archivo',sans-serif;box-shadow:0 12px 28px rgba(0,46,95,.18);cursor:pointer}
2111.commercial-autosave-note{display:inline-flex;align-items:center;gap:7px;border:1px solid rgba(23,112,77,.18);border-radius:999px;background:#e7f5ee;color:#17704d;padding:10px 13px;font:900 11px 'Archivo',sans-serif}
2112.commercial-autosave-note img{width:15px;height:15px}
2113.commercial-prices-link{background:#fff;color:#003b73;border:1px solid #cfe0f1;box-shadow:0 10px 24px rgba(15,42,70,.08)}
2114.commercial-save img,.commercial-prices-link img{width:16px;height:16px}
2115.commercial-save img{filter:brightness(0) invert(1)}
2116.price-board-story-export{background:linear-gradient(135deg,#833ab4 0%,#fd1d1d 50%,#fcb045 100%);color:#fff;border-color:transparent;box-shadow:0 10px 24px rgba(193,53,132,.22)}
2117.price-board-story-export img{filter:brightness(0) invert(1)}
2118.price-board-page{min-height:100vh;background:radial-gradient(circle at 12% 8%,rgba(55,162,196,.22),transparent 26%),linear-gradient(135deg,#edf5fb 0%,#f8fbff 46%,#fff 100%);color:#10233b;padding:26px clamp(12px,3vw,34px) 84px}
2119.price-board-shell{width:min(1180px,100%);margin:0 auto}
2120@media(max-width:760px){body:has(.price-board-page) .app-mobile-load-fab{display:none!important}.price-board-page{padding-left:14px!important;padding-right:14px!important}}
2121.price-board-hero{position:relative;overflow:hidden;border-radius:18px;background:linear-gradient(135deg,#002e5f 0%,#075995 58%,#16a4b4 100%);color:#fff;padding:30px;box-shadow:0 22px 54px rgba(0,46,95,.2)}
2122.price-board-hero:after{content:"";position:absolute;inset:-40% -12% auto auto;width:460px;height:460px;background:radial-gradient(circle,rgba(255,255,255,.2),transparent 63%);filter:blur(8px)}
2123.price-board-hero-content{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:28px;align-items:center}
2124.price-board-kicker{display:block;font-size:11px;font-weight:950;letter-spacing:.14em;text-transform:uppercase;color:#9fe6ee}
2125.price-board-hero h1{margin:7px 0 8px;font-size:clamp(34px,5vw,58px);line-height:.96;letter-spacing:0;color:#fff}
2126.price-board-hero p{margin:0;color:rgba(255,255,255,.82);font-size:15px;line-height:1.45;max-width:620px}
2127.price-board-logos{display:grid;gap:10px;justify-items:stretch}
2128.price-board-brand-row{display:grid;grid-template-columns:.82fr 1.18fr;gap:10px}
2129.price-board-main-logo,.price-board-rosgan-logo{border-radius:14px;background:#fff;min-height:94px;display:grid;place-items:center;padding:16px;border:1px solid rgba(255,255,255,.72);box-shadow:0 16px 34px rgba(0,46,95,.16)}
2130.price-board-main-logo img,.price-board-rosgan-logo img{max-width:100%;max-height:70px;object-fit:contain}
2131.price-board-logo-fallback{font-size:24px;font-weight:950;color:#1f2d4d;letter-spacing:.04em;text-align:center}
2132.price-board-cons-card{display:flex;align-items:center;gap:10px;min-height:48px;border-radius:999px;background:rgba(255,255,255,.14);color:#fff;padding:8px 12px;border:1px solid rgba(255,255,255,.26);backdrop-filter:blur(10px);box-sizing:border-box}
2133rosgan-price-board{display:block;width:100%;margin:20px auto}rosgan-price-board svg{display:block;width:100%;height:auto;margin-bottom:20px}.price-board-view-switch{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.price-board-view-switch button{padding:12px 18px;border:1px solid #cbd9e6;border-radius:24px;color:#102f5d;background:white;font-weight:700;cursor:pointer}.price-board-view-switch button[aria-pressed="true"]{background:#102f5d;color:white}.price-board-cons-avatar{flex:0 0 auto;width:34px;height:34px;display:grid;place-items:center;border-radius:50%;padding:8px;box-sizing:border-box;background:#d91549;line-height:0}.price-board-cons-avatar img{display:block;width:auto!important;height:auto!important;max-width:18px!important;max-height:18px!important;object-fit:contain;object-position:center}.price-board-cons-card div{min-width:0;display:flex;align-items:baseline;gap:8px}.price-board-cons-card small{display:block;color:rgba(255,255,255,.72);font-size:9px;font-weight:900;letter-spacing:.1em;text-transform:uppercase;white-space:nowrap}.price-board-cons-name{display:block;min-width:0;font-size:13px;font-weight:900;line-height:14px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2134.price-board-toolbar{display:flex;justify-content:flex-end;
2134gap:12px;align-items:center;margin:20px 0 18px;flex-wrap:wrap}
2135.price-board-updated{color:#65758a;font-size:12px;font-weight:800}
2136.price-board-grid{display:grid;gap:10px}
2137.price-category-card{position:relative;overflow:hidden;border:1px solid #dce7f2;border-radius:12px;background:rgba(255,255,255,.98);box-shadow:0 12px 30px rgba(15,42,70,.055)}
2138.price-category-card:before{content:"";position:absolute;inset:0 auto 0 0;width:5px;background:linear-gradient(180deg,#0a5ca8,#22b4bd)}
2139.price-category-inner{display:grid;grid-template-columns:minmax(250px,.85fr) minmax(0,2.65fr);gap:12px;align-items:center;padding:13px 14px 13px 22px}
2140.price-category-name{display:flex;flex-direction:column;justify-content:center;min-width:0}.price-category-name small{color:#6f8198;font-size:9px;font-weight:950;letter-spacing:.1em;text-transform:uppercase}.price-category-name strong{margin-top:5px;color:#082f5c;font-size:17px;line-height:1.08;letter-spacing:0}
2141.price-category-spark{position:relative;height:72px;border:1px solid #dfeaf4;border-radius:10px;background:linear-gradient(180deg,#f9fcff,#fff);overflow:hidden}.price-category-spark:before{content:"";position:absolute;inset:8px 10px 24px;background:repeating-linear-gradient(180deg,rgba(0,46,95,.07) 0 1px,transparent 1px 16px)}.price-category-spark svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.price-category-spark polyline{fill:none;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 5px 7px rgba(0,46,95,.12));stroke-dasharray:360;stroke-dashoffset:0;transform-origin:right center}.price-category-spark circle{filter:drop-shadow(0 4px 6px rgba(0,46,95,.18));transform-origin:center}.price-category-spark.is-a polyline{animation:priceTrendSlideA .78s cubic-bezier(.2,.72,.18,1) both}.price-category-spark.is-b polyline{animation:priceTrendSlideB .78s cubic-bezier(.2,.72,.18,1) both}.price-category-spark.is-a circle{animation:priceTrendDotA .62s cubic-bezier(.2,.72,.18,1) both}.price-category-spark.is-b circle{animation:priceTrendDotB .62s cubic-bezier(.2,.72,.18,1) both}.price-category-spark span{position:absolute;left:10px;right:10px;bottom:6px;height:14px;display:flex;align-items:center;background:linear-gradient(90deg,#f9fcff 0 58%,rgba(249,252,255,0));color:#75869a;font-size:8px;font-weight:950;text-transform:uppercase;letter-spacing:.08em;z-index:2}
2142@keyframes priceTrendSlideA{0%{stroke-dashoffset:150;opacity:.35;transform:translateX(14px) scaleX(1.08)}60%{opacity:1;transform:translateX(-2px) scaleX(.985)}100%{stroke-dashoffset:0;opacity:1;transform:translateX(0) scaleX(1)}}@keyframes priceTrendSlideB{0%{stroke-dashoffset:150;opacity:.35;transform:translateX(14px) scaleX(1.08)}60%{opacity:1;transform:translateX(-2px) scaleX(.985)}100%{stroke-dashoffset:0;opacity:1;transform:translateX(0) scaleX(1)}}@keyframes priceTrendDotA{0%{opacity:.25;transform:translateX(10px) scale(.45)}55%{transform:translateX(0) scale(1.35)}100%{opacity:1;transform:translateX(0) scale(1)}}@keyframes priceTrendDotB{0%{opacity:.25;transform:translateX(10px) scale(.45)}55%{transform:translateX(0) scale(1.35)}100%{opacity:1;transform:translateX(0) scale(1)}}
2143.price-stat-grid{display:grid;grid-template-columns:.58fr 1fr 1.12fr;gap:8px}
2144.price-stat-cell{min-height:56px;padding:9px 11px;border:1px solid #e1ebf4;border-radius:9px;background:linear-gradient(180deg,#fff,#fbfdff);display:grid;gap:5px;align-content:center}.price-stat-cell small{color:#75869a;font-size:8.5px;font-weight:950;letter-spacing:.08em;text-transform:uppercase}.price-roll{display:inline-flex;align-items:baseline;gap:4px;color:#002e5f;font-size:clamp(18px,2vw,25px);font-weight:950;line-height:1;font-variant-numeric:tabular-nums;white-space:nowrap}.price-stat-cell.is-count{background:#edf7fb;border-color:#cde6f1}.price-stat-cell.is-count .price-roll{font-size:clamp(22px,2.4vw,30px)}.price-stat-cell.is-average{background:linear-gradient(135deg,#002e5f,#0a5ca8);border-color:#0a5ca8}.price-stat-cell.is-average small,.price-stat-cell.is-average .price-roll{color:#fff}.price-roll em{font-style:normal;font-size:12px;color:#6f8198;font-weight:900}
2145.price-board-empty{border:1px solid #dbe7f2;border-radius:14px;background:#fff;padding:38px 18px;text-align:center;color:#65758a;font-weight:800;box-shadow:0 14px 34px rgba(15,42,70,.06)}
2146@media(max-width:760px){.price-board-page{padding:18px 6px 74px}.price-board-hero{border-radius:14px;padding:18px}.price-board-hero-content{grid-template-columns:1fr;gap:18px}.price-board-logos{grid-template-columns:1fr}.price-board-brand-row{grid-template-columns:1fr 1fr}.price-board-main-logo,.price-board-rosgan-logo{min-height:76px;padding:12px}.price-board-main-logo img,.price-board-rosgan-logo img{max-height:54px}.price-board-logo-fallback{font-size:18px}.price-board-cons-card{min-height:44px}.price-board-cons-card strong{font-size:12px}.price-board-grid{gap:7px}.price-category-card{border-radius:10px}.price-category-card:before{width:4px}.price-category-inner{grid-template-columns:1fr;gap:7px;padding:8px 6px 7px 10px}.price-category-name small{font-size:6px;letter-spacing:.04em}.price-category-name strong{margin-top:3px;font-size:10px;line-height:1.05;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.price-stat-grid{grid-template-columns:minmax(42px,48px) repeat(2,minmax(0,1fr));gap:3px}.price-stat-cell{min-height:43px;padding:5px 3px;border-radius:7px;gap:3px}.price-stat-cell.is-count{max-width:48px;padding-left:5px;padding-right:5px}.price-stat-cell small{font-size:5.7px;letter-spacing:0}.price-board-toolbar{justify-content:flex-start;margin:13px 0 10px}.price-roll{font-size:11px;gap:0}.price-stat-cell.is-count .price-roll{font-size:11px}}
2147@media(max-width:980px) and (min-width:761px){.price-board-hero-content{grid-template-columns:1fr 320px}.price-board-hero h1{font-size:48px}.price-board-main-logo{min-height:104px}}
2148.commercial-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:20px 0 12px}
2149.commercial-metric{display:grid;grid-template-columns:42px minmax(0,1fr);gap:12px;align-items:center;min-height:78px;padding:14px 15px;border:1px solid #e0e9f2;border-radius:8px;background:#fff;box-shadow:0 10px 28px rgba(15,42,70,.06)}
2150.commercial-metric-icon{width:42px;height:42px;display:grid;place-items:center;border-radius:50%;background:#e8f2fc}
2151.commercial-metric-icon img{width:20px;height:20px}
2152.commercial-metric small{display:block;color:#6e7f94;font-size:11px;font-weight:850}
2153.commercial-metric strong{display:inline-block;margin-top:3px;color:#0b315d;font-size:24px;line-height:1}
2154.commercial-metric em{margin-left:7px;color:#8593a6;font-size:11px;font-style:normal;font-weight:800}
2155.commercial-filters{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0 14px}
2156.commercial-filter{display:inline-flex;align-items:center;gap:8px;min-height:38px;border:1px solid #d8e5f1;border-radius:999px;background:#fff;color:#31526f;padding:8px 13px;font:900 12px 'Archivo',sans-serif;box-shadow:0 7px 18px rgba(15,42,70,.05);cursor:pointer}
2157.commercial-filter.is-active{background:#002e5f;border-color:#002e5f;color:#fff}
2158.commercial-filter b{min-width:21px;height:21px;
2158display:grid;place-items:center;border-radius:999px;background:rgba(10,92,168,.1);font-size:11px}
2159.commercial-filter.is-active b{background:rgba(255,255,255,.2)}
2160.commercial-board{overflow:visible;border:1px solid #dce7f2;border-radius:8px;background:#fff;box-shadow:0 18px 42px rgba(15,42,70,.08)}
2161.commercial-table-head,.commercial-row-main{display:grid;grid-template-columns:48px 58px 72px 82px minmax(160px,1.3fr) 58px minmax(230px,1.55fr) minmax(170px,1.05fr) 40px;align-items:center;gap:8px}
2162
2163.commercial-table-head{position:sticky;top:58px;z-index:20;min-height:48px;padding:0 16px;border-radius:8px 8px 0 0;background:rgba(246,249,252,.97);color:#71839a;font-size:10px;font-weight:950;letter-spacing:.08em;text-transform:uppercase;box-shadow:0 1px 0 #dce7f2,0 8px 18px rgba(15,42,70,.08);backdrop-filter:blur(8px)}
2164html[data-public-route="lacassina-commercial"] .commercial-table-head{top:0}
2165.commercial-row{border-top:1px solid #e7eef5;background:#fff}
2166.commercial-row-main{padding:12px 16px}
2167.commercial-row-index,.commercial-row-strong,.commercial-category{color:#10233b;font-size:13px;font-weight:950;line-height:1.15}
2168.commercial-order-badge{width:30px;height:30px;display:grid;place-items:center;border-radius:50%;background:linear-gradient(145deg,#0a5ca8,#002e5f);color:#fff;font-size:12px;font-weight:950;line-height:1;box-shadow:0 7px 16px rgba(0,46,95,.22)}
2169.commercial-row-muted{display:none;color:#65758a;font-size:11px;font-weight:850;line-height:1.1}
2170.commercial-category{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2171.commercial-cons-avatar{width:34px;height:34px;display:grid;place-items:center;border-radius:50%;padding:7px;box-sizing:border-box;background:#d91549;box-shadow:0 8px 18px rgba(217,21,73,.2);line-height:0}
2172.commercial-cons-avatar img{display:block;width:auto!important;height:auto!important;max-width:20px!important;max-height:20px!important;object-fit:contain;object-position:center}
2173.commercial-cons-avatar strong{color:#fff;font-size:13px}
2174.commercial-status{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
2175.commercial-status button{min-height:28px;border:1px solid #dfe7ef;border-radius:6px;background:#f8fafc;color:#66778c;font:850 10px 'Archivo',sans-serif;cursor:pointer}
2176.commercial-status .is-vendido{background:#e7f5ee;border-color:#a9dcc4;color:#17704d}
2177.commercial-status .is-pendiente{background:#fff6df;border-color:#f3cf78;color:#9a6700}
2178.commercial-status .is-no-vendido{background:#fff0f0;border-color:#f1b4b4;color:#c03434}
2179.commercial-status .is-levantado{background:#eef2f6;border-color:#cbd8e5;color:#4e6279}
2180.commercial-status-pill{display:none;align-items:center;justify-content:center;min-height:28px;border:1px solid;border-radius:999px;padding:5px 8px;font:950 9px 'Archivo',sans-serif;text-align:center;line-height:1.05;white-space:nowrap}
2181.commercial-price{display:grid;gap:6px}
2182.commercial-mode{display:grid;grid-template-columns:1fr 1fr;border:1px solid #d7e3ef;border-radius:8px;overflow:hidden;background:#f7fafd}
2183.commercial-mode button{min-height:28px;border:0;background:transparent;color:#65758a;font:900 10px 'Archivo',sans-serif;cursor:pointer}
2184.commercial-mode button.is-active{background:#0f7b48;color:#fff}
2185.commercial-price input,.commercial-more-grid input{width:100%;min-height:34px;border:1px solid #d8e4f0;border-radius:8px;background:#fff;color:#10233b;padding:8px 10px;box-sizing:border-box;font:850 12px 'Archivo',sans-serif;outline:0}
2186.commercial-price input:focus,.commercial-more-grid input:focus{border-color:#0a5ca8;box-shadow:0 0 0 3px rgba(10,92,168,.12)}
2187.commercial-price input:disabled{background:#eef3f8;color:#8a98aa;cursor:not-allowed}
2188.commercial-mode button:disabled{opacity:.55;cursor:not-allowed}
2189.commercial-faena{display:grid;grid-template-columns:40px minmax(0,1fr);gap:8px;align-items:center}
2190.commercial-switch{width:40px;height:24px;border:0;border-radius:999px;background:#c7d1dc;padding:3px;cursor:pointer;transition:background-color .18s ease}
2191.commercial-switch span{display:block;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 2px 5px rgba(0,0,0,.2);transition:transform .18s ease}
2192.commercial-switch.is-on{background:#177b4d}
2193.commercial-switch.is-on span{transform:translateX(16px)}
2194.commercial-faena input{width:100%;min-height:30px;border:1px solid #d8e4f0;border-radius:999px;background:#f8fbfe;color:#177b4d;text-align:center;font:900 11px 'Archivo',sans-serif;outline:0}
2195.commercial-more-button{width:34px;height:34px;display:grid;place-items:center;border:1px solid #d7e3ef;border-radius:10px;background:#fff;cursor:pointer}
2196.commercial-more-button img{width:17px;height:17px;transition:transform .18s ease}
2197.commercial-more{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;max-height:0;margin:0 16px;padding:0 15px;border:1px solid transparent;border-radius:8px;background:#fbfdff;overflow:hidden;opacity:0;transform:translateY(-6px) scaleY(.98);transform-origin:top;pointer-events:none;transition:max-height .24s cubic-bezier(.2,.72,.24,1),opacity .18s ease,transform .24s cubic-bezier(.2,.72,.24,1),padding .24s cubic-bezier(.2,.72,.24,1),margin .24s cubic-bezier(.2,.72,.24,1),border-color .18s ease}
2198.commercial-more.is-open{max-height:620px;margin:0 16px 14px;padding:15px;border-color:#dbe7f2;opacity:1;transform:translateY(0) scaleY(1);pointer-events:auto}
2199@media(prefers-reduced-motion:reduce){.commercial-more,.commercial-more-button img{transition:none}}
2200.commercial-more-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
2201.commercial-more-grid label{display:grid;gap:6px;color:#65758a;font-size:10px;font-weight:950;letter-spacing:.06em;text-transform:uppercase}
2202.commercial-more-status{display:none}
2203.commercial-category-divider{display:none}
2204.commercial-empty{padding:42px 18px;text-align:center;color:#65758a;font-weight:800}
2205.commercial-view-tabs{display:inline-flex;gap:4px;margin:0 0 14px;padding:4px;border:1px solid #d8e5f1;border-radius:10px;background:#eaf1f7}
2206.commercial-view-tab{display:inline-flex;align-items:center;gap:8px;min-height:36px;border:0;border-radius:7px;background:transparent;color:#4e6379;padding:8px 13px;font:900 11px 'Archivo',sans-serif;cursor:pointer}
2207.commercial-view-tab img{width:15px;height:15px}.commercial-view-tab.is-active{background:#fff;color:#003b73;box-shadow:0 5px 14px rgba(15,42,70,.1)}
2208.commercial-report{overflow:hidden;border:1px solid #dce7f2;border-radius:10px;background:#fff;box-shadow:0 18px 42px rgba(15,42,70,.08)}
2209.commercial-report-head{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:18px 20px;border-bottom:1px solid #e2ebf3;background:linear-gradient(135deg,#f7fbff,#eef6fb)}
2210.commercial-report-head h2{margin:0;color:#0b315d;font-size:20px}.commercial-report-head p{margin:4px 0 0;color:#65758a;font-size:12px;line-height:1.4}
2211.commercial-export-excel{display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:0 0 auto;border:0;border-radius:9px;background:#17704d;color:#fff;padding:11px 15px;font:900 12px 'Archivo',sans-serif;box-shadow:0 10px 22px rgba(23,112,77,.2);cursor:pointer}.commercial-export-excel img{width:17px;height:17px;filter:brightness(0) invert(1)}
2212.commercial-report-scroll{overflow:auto;max-height:min(62vh,680px)}
2213.commercial-report-table{width:100%;min-width:1140px;border-collapse:separate;border-spacing:0;font-size:12px}
2214.commercial-report-table thead th{position:sticky;top:0;z-index:3;padding:12px 10px;background:#003b73;color:#fff;text-align:left;font-size:9px;font-weight:950;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap}
2215.commercial-report-table tbody td{padding:11px 10px;border-bottom:1px solid #e7eef5;color:#1b3148;font-weight:750;vertical-align:middle}.commercial-report-table tbody tr:nth-child(even) td{background:#f8fbfd}.commercial-report-table tbody tr:hover td{background:#eef6fc}
2216.commercial-report-table .is-number{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}.commercial-report-table .is-strong{color:#082f5c;font-weight:950}.commercial-report-table .is-muted{color:#7a899a}.commercial-report-status{display:inline-flex;align-items:center;justify-content:center;border:1px solid;border-radius:999px;padding:5px 8px;font-size:9px;font-weight:950;white-space:nowrap}
2217.commercial-report-foot{display:flex;justify-content:space-between;gap:12px;padding:12px 20px;background:#f7fafc;color:#65758a;font-size:11px;font-weight:800}
2218@media(max-width:960px){
2219  .commercial-hero{grid-template-columns:1fr}.commercial-save{justify-self:start}
2220  .commercial-summary{grid-template-columns:repeat(2,minmax(0,1fr))}
2221  .commercial-table-head{display:none}
2222  .commercial-board{display:grid;gap:10px;border:0;background:transparent;box-shadow:none}
2223  .commercial-report-head{align-items:flex-start;flex-direction:column}.commercial-export-excel{width:100%}.commercial-report{border-radius:8px}
2223.commercial-report-foot{flex-direction:column}
2224  .commercial-row{border:1px solid #dce7f2;border-radius:12px;overflow:hidden;box-shadow:0 10px 26px rgba(15,42,70,.06)}
2225  .commercial-row-main{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:14px}
2226  .commercial-row-main>div{min-width:0}
2227  .commercial-row-muted{display:block}
2228  .commercial-mobile-span{grid-column:1/-1}
2229  .commercial-category{white-space:normal;line-height:1.28}
2230  .commercial-status{grid-template-columns:repeat(4,minmax(0,1fr))}
2231  .commercial-more-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
2232}
2233@media(max-width:560px){
2234  .commercial-page{padding:18px 10px 88px}
2235  .commercial-summary{grid-template-columns:1fr 1fr;gap:8px}
2236  .commercial-metric{grid-template-columns:34px minmax(0,1fr);min-height:68px;padding:10px}.commercial-metric-icon{width:34px;height:34px}.commercial-metric strong{font-size:20px}
2237  .commercial-filters{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));overflow:visible;gap:6px;margin:9px 0 11px;padding:0}
2238  .commercial-filter{min-width:0;width:100%;justify-content:space-between;min-height:34px;padding:6px 10px;gap:6px;font-size:10px;box-shadow:none}
2239  .commercial-filter:last-child:nth-child(odd){grid-column:1/-1}
2240  .commercial-filter b{min-width:17px;height:17px;font-size:9px}
2241  .commercial-board{gap:7px}
2242  .commercial-category-divider{display:flex;align-items:center;gap:8px;margin:10px 4px 2px;color:#0a5ca8;font:950 10px 'Archivo',sans-serif;letter-spacing:.08em;text-transform:uppercase}
2243  .commercial-category-divider:after{content:"";height:1px;flex:1;background:#cfe0f1}
2244  .commercial-row{border-radius:10px}
2245  .commercial-row-main{grid-template-columns:31px 31px minmax(54px,.75fr) 36px 28px minmax(110px,1.35fr) 28px;gap:5px;align-items:center;padding:9px 10px}
2246  .commercial-row-main>div{min-width:0}
2247  .commercial-row-main>div:nth-child(1),.commercial-row-main>div:nth-child(2),.commercial-row-main>div:nth-child(3),.commercial-row-main>div:nth-child(4){align-self:center}
2248  .commercial-row-main>div:nth-child(5){display:none}
2249  .commercial-row-main>div:nth-child(6){display:flex;align-items:center;justify-content:center}
2250  .commercial-row-main>div:nth-child(1),.commercial-row-main>div:nth-child(2),.commercial-row-main>div:nth-child(3),.commercial-row-main>div:nth-child(4),.commercial-row-main>div:nth-child(6),.commercial-row-main>div:nth-child(8),.commercial-row-main>.commercial-more-button{order:1}
2251  .commercial-row-main>div:nth-child(7){order:2;grid-column:1/-1;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:5px}
2252  .commercial-order-badge{width:28px;height:28px;font-size:11px}
2253  .commercial-row-muted{font-size:7px;letter-spacing:0;text-transform:uppercase;color:#7b8ca1;white-space:nowrap}
2254  .commercial-row-index,.commercial-row-strong{font-size:12px;line-height:1.05}
2255  .commercial-cons-avatar{width:28px;height:28px;padding:6px}
2256  .commercial-cons-avatar img{max-width:16px!important;max-height:16px!important}
2257  .commercial-row-main>.commercial-status button{display:block;min-height:25px;border-radius:7px;padding:4px 3px;font-size:8.5px;line-height:1.05;white-space:normal}
2258  .commercial-status-pill{display:none}
2259  .commercial-price{gap:4px}
2260  .commercial-mode{grid-template-columns:1fr 1fr;border-radius:7px}
2261  .commercial-mode button{min-height:24px;font-size:9px}
2262  .commercial-price input{min-height:30px;border-radius:7px;padding:6px 8px;font-size:12px}
2263  .commercial-more-button{width:30px;height:30px;border-radius:8px}
2264  .commercial-more-button img{width:15px;height:15px}
2265  .commercial-more{margin:0 9px;padding:0 12px;gap:10px}
2266  .commercial-more.is-open{margin:0 9px 9px;padding:12px}
2267  .commercial-more-status{display:none}
2268  .commercial-more-grid{grid-template-columns:1fr}
2269  .commercial-report-scroll{overflow:hidden;max-height:none;padding:8px;background:#f4f8fb}
2270  .commercial-report-table{display:block;width:100%;min-width:0;border-collapse:separate;font-size:11px}
2271  .commercial-report-table thead{display:none}
2272  .commercial-report-table tbody{display:grid;gap:8px}
2273  .commercial-report-table tbody tr{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px 12px;padding:12px;border:1px solid #dce7f2;border-radius:10px;background:#fff;box-shadow:0 5px 14px rgba(15,42,70,.05)}
2274  .commercial-report-table tbody td,.commercial-report-table tbody tr:nth-child(even) td,.commercial-report-table tbody tr:hover td{display:grid;min-width:0;gap:3px;padding:0;border:0;background:transparent;text-align:left;white-space:normal;overflow-wrap:anywhere}
2275  .commercial-report-table tbody td::before{content:attr(data-label);color:#7a899a;font-size:8px;font-weight:950;letter-spacing:.07em;text-transform:uppercase}
2276  .commercial-report-table tbody td[data-label="Categoría"],.commercial-report-table tbody td[data-label="Cabaña"],.commercial-report-table tbody td[data-label="Consignataria"]{grid-column:1/-1}
2277  .commercial-report-table .is-number{text-align:left;white-space:normal}
2278}
2279.mav-chart-wrap,.market-heat-table-wrap{max-width:100%}
2280@media(max-width:760px){
2281  .mav-chart-wrap{overflow:hidden!important}
2282  .mav-chart-canvas{width:100%;min-width:0!important}
2283  .market-heat-table-wrap{overflow:hidden!important}
2284  .market-heat-table{width:100%!important;min-width:0!important;table-layout:fixed}
2285  .market-heat-table th,.market-heat-table td{min-width:0!important;padding:7px 3px!important;font-size:8.5px!important;white-space:normal!important;overflow-wrap:anywhere}
2286  .market-heat-table th:first-child,.market-heat-table td:first-child{position:static!important;width:27%;padding-left:7px!important;text-align:left!important}
2287  .market-heat-table td>div{padding:7px 2px!important;font-size:8.5px!important;border-radius:6px!important}
2288  .market-chip-list,.market-year-list{flex-wrap:wrap!important;overflow-x:visible!important;padding-bottom:0!important}
2289  .market-chip-list>*,.market-year-list>*{flex-shrink:1!important}
2290}
2291.advance-splash{min-height:100vh;display:grid;place-items:center;overflow:hidden;position:relative;background:#003568}
2292.advance-splash::before,.advance-splash::after{content:"";position:absolute;inset:-35%;background:radial-gradient(circle at 42% 48%,rgba(31,133,211,.5),transparent 36%),linear-gradient(115deg,transparent 32%,rgba(255,255,255,.08) 50%,transparent 68%);animation:advanceDrift 12s ease-in-out infinite alternate}
2293.advance-splash::after{inset:-20%;opacity:.45;animation-duration:18s;animation-direction:alternate-reverse}
2294.advance-splash-content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;width:min(760px,88vw);text-align:center;color:#fff}
2295.advance-splash img{position:relative;width:min(360px,58vw);height:auto;filter:brightness(0) invert(1);animation:advanceLogo 3.8s ease-in-out infinite}
2296.advance-splash-copy{margin-top:clamp(32px,5vh,54px);animation:advanceCopyIn .8s .18s cubic-bezier(.2,.72,.18,1) both}
2297.advance-splash-copy h1{margin:0;font-size:clamp(25px,4.2vw,42px);font-weight:800;line-height:1.08;letter-spacing:-.025em;text-wrap:balance}
2298.advance-splash-copy p{margin:11px 0 0;color:rgba(255,255,255,.78);font-size:clamp(15px,2vw,20px);font-weight:500;line-height:1.45;text-wrap:balance}
2299.advance-splash-actions{display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:clamp(25px,4vh,36px)}
2300.advance-splash-launch{display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:50px;padding:0 23px;border:0;border-radius:999px;background:#fff;color:#003568;font:850 14px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 16px 38px rgba(0,20,45,.25);transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease}
2301.advance-splash-launch svg{width:18px;height:18px}.advance-splash-launch:hover{transform:translateY(-2px);box-shadow:0 20px 45px rgba(0,20,45,.32)}.advance-splash-launch:disabled{cursor:wait;opacity:.72;transform:none}
2302.advance-splash-launch.is-subscribed{background:#6be2bd;color:#003d35;cursor:default}
2303.advance-splash-interest-note{max-width:520px!important;margin:2px 0 0!important;color:rgba(255,255,255,.72)!important;font-size:11.5px!important;line-height:1.45!important}
2304.advance-splash-interest-message{max-width:520px!important;margin:0!important;color:#fff!important;font-size:12px!important;font-weight:750!important}
2305.advance-splash-unsubscribe{border:0;background:transparent;color:rgba(255,255,255,.67);font:650 11.5px 'Archivo',sans-serif;text-decoration:underline;text-underline-offset:3px;
2305cursor:pointer}.advance-splash-unsubscribe:disabled{opacity:.5;cursor:wait}
2306.advance-splash-logout{display:inline-flex;align-items:center;justify-content:center;gap:9px;margin-top:clamp(26px,4vh,38px);min-height:44px;padding:0 19px;border:1px solid rgba(255,255,255,.28);border-radius:999px;background:rgba(255,255,255,.09);backdrop-filter:blur(12px);color:rgba(255,255,255,.9);font:700 13px 'Archivo',sans-serif;letter-spacing:.01em;cursor:pointer;transition:transform .2s ease,background .2s ease,border-color .2s ease,box-shadow .2s ease}
2307.advance-splash-logout svg{width:17px;height:17px;transition:transform .2s ease}
2308.advance-splash-logout:hover{transform:translateY(-2px);background:rgba(255,255,255,.17);border-color:rgba(255,255,255,.46);box-shadow:0 12px 30px rgba(0,22,49,.18)}
2309.advance-splash-logout:hover svg{transform:translateX(2px)}
2310.advance-splash-logout:active{transform:translateY(0) scale(.98)}
2311@keyframes advanceDrift{to{transform:translate3d(8%,4%,0) scale(1.08)}}
2312@keyframes advanceLogo{0%,100%{transform:translateY(0);opacity:.9}50%{transform:translateY(-8px);opacity:1}}
2313@keyframes advanceCopyIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
2314.app-welcome{min-height:100vh;position:relative;overflow:hidden;display:grid;place-items:center;padding:28px;box-sizing:border-box;background:radial-gradient(circle at 18% 18%,rgba(54,180,206,.34),transparent 31rem),radial-gradient(circle at 84% 78%,rgba(21,111,191,.5),transparent 36rem),linear-gradient(145deg,#001d3d 0%,#00457f 54%,#087f9d 100%);color:#fff}
2315::view-transition-old(root),::view-transition-new(root){mix-blend-mode:normal;animation-duration:.52s;animation-timing-function:cubic-bezier(.2,.72,.2,1)}
2316::view-transition-old(root){animation-name:rosganViewOut}
2317::view-transition-new(root){animation-name:rosganViewIn}
2318.app-welcome::before,.app-welcome::after{content:"";position:absolute;border:1px solid rgba(255,255,255,.16);border-radius:50%;animation:welcomeOrbit 9s ease-in-out infinite alternate}
2319.app-welcome::before{width:min(68vw,760px);aspect-ratio:1;left:-22vw;top:-38vw;box-shadow:0 0 0 70px rgba(255,255,255,.025),0 0 0 150px rgba(255,255,255,.018)}
2320.app-welcome::after{width:min(48vw,540px);aspect-ratio:1;right:-17vw;bottom:-25vw;animation-direction:alternate-reverse;animation-duration:12s}
2321.app-welcome-content{position:relative;z-index:1;width:min(720px,92vw);display:flex;flex-direction:column;align-items:center;text-align:center;animation:welcomeContentIn .72s cubic-bezier(.18,.76,.22,1) both}
2322.app-welcome-logo{width:min(210px,44vw);height:auto;filter:brightness(0) invert(1);animation:welcomeLogoFloat 3s ease-in-out infinite}
2323.app-welcome-lottie-slot{display:grid;place-items:center;width:clamp(168px,25vh,240px);height:clamp(168px,25vh,240px);margin:-2px 0 -14px}
2324.app-welcome-lottie,.app-connecting-lottie,dotlottie-wc[classname="app-welcome-lottie"],dotlottie-wc[classname="app-connecting-lottie"]{grid-area:1/1;display:block;width:100%;height:100%;filter:drop-shadow(0 18px 32px rgba(0,23,44,.18))}
2325.app-welcome-kicker{display:inline-flex;align-items:center;gap:8px;margin-top:0;padding:7px 13px;border:1px solid rgba(255,255,255,.22);border-radius:999px;background:rgba(255,255,255,.1);backdrop-filter:blur(12px);font-size:10px;font-weight:850;letter-spacing:.14em;text-transform:uppercase;color:#bfe9f2}
2326.app-welcome-kicker::before{content:"";width:7px;height:7px;border-radius:50%;background:#69e4bd;box-shadow:0 0 0 5px rgba(105,228,189,.12);animation:pulseDot 1.4s infinite}
2327.app-welcome h1{margin:18px 0 0;font-size:clamp(34px,6vw,64px);line-height:1;letter-spacing:-.04em;text-wrap:balance}
2328.app-welcome.app-connecting-screen{background:radial-gradient(ellipse at 50% 42%,#fff 0%,#f4f8fc 50%,#eaf1f8 100%);color:#002e5f;padding:32px 20px;overflow-y:auto}
2329.app-connecting-screen::before,.app-connecting-screen::after{display:none}
2330.app-connecting-screen .app-welcome-content{width:min(560px,100%);animation:none}
2331.app-connecting-screen .app-welcome-lottie-slot{width:min(420px,82vw,53svh);height:auto;aspect-ratio:1;margin:0 0 18px}
2332.app-connecting-screen .app-connecting-lottie{filter:none}
2333.app-connecting-screen .app-welcome-kicker{color:#217864;background:#e7f3ef;border-color:#d6eae3;font-size:9px;letter-spacing:.13em;padding:7px 12px}
2334.app-connecting-screen h1{font-size:clamp(25px,4vw,36px);line-height:1.15;margin:18px 0 0;letter-spacing:-.035em}
2335.app-connecting-description{max-width:340px;margin:12px 0 0;font-size:14px;line-height:1.6;color:#647a90;text-wrap:balance}
2336.app-connection-success .app-welcome-kicker::before{animation:none;background:#148660;box-shadow:none}
2337.app-connection-success h1,.app-connection-success .app-connecting-description{animation:connectionConfirmed .5s ease-out both}
2338.app-connection-success .app-welcome-lottie{filter:none}
2339@keyframes connectionConfirmed{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
2340@media(prefers-reduced-motion:reduce){.app-connection-success h1,.app-connection-success .app-connecting-description{animation:none}}
2341@keyframes welcomeOrbit{to{transform:translate3d(4%,6%,0) scale(1.08);opacity:.6}}
2342@keyframes welcomeContentIn{from{opacity:0;transform:translateY(22px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}}
2343@keyframes welcomeLogoFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
2344@keyframes rosganViewOut{to{opacity:0;transform:translateY(-8px) scale(.992);filter:blur(2px)}}
2345@keyframes rosganViewIn{from{opacity:0;transform:translateY(12px) scale(.992);filter:blur(2px)}to{opacity:1;transform:none;filter:none}}
2346.auth-view-bridge{position:fixed;inset:0;z-index:30000;pointer-events:none;opacity:0;background:radial-gradient(circle at 50% 45%,rgba(41,178,201,.36),transparent 42%),linear-gradient(135deg,#00264f,#076d96);animation:authViewBridge .56s cubic-bezier(.2,.72,.2,1) both}
2347.auth-view-bridge::after{content:"";position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,rgba(255,255,255,.11),transparent 28%);transform:scale(.72);animation:authBridgeGlow .56s ease both}
2348@keyframes authViewBridge{0%{opacity:0}38%,57%{opacity:1}100%{opacity:0}}
2349@keyframes authBridgeGlow{0%{opacity:0;transform:scale(.72)}48%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(1.14)}}
2350@media(prefers-reduced-motion:reduce){::view-transition-old(root),::view-transition-new(root),.app-welcome::before,.app-welcome::after,.app-welcome-content,.app-welcome-logo,.auth-view-bridge,.auth-view-bridge::after{animation:none}}
2351.profile-company{display:flex;align-items:center;gap:14px;padding:12px 16px;background:#fff;border:1px solid #e3eaf1;border-left:3px solid #002e5f;border-radius:10px;margin-bottom:16px}
2352.profile-company img{width:64px;height:36px;object-fit:contain}.profile-company span,.profile-lot-cert{font-size:10px;letter-spacing:.04em;font-weight:700;color:#627b94}.profile-company h2{margin:3px 0 0;font-size:15px;color:#002e5f}
2353.profile-rosgan-lots{background:#fff;border:1px solid #e3eaf1;border-radius:12px;padding:16px 18px;margin-bottom:18px}.profile-rosgan-lots header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.profile-rosgan-lots h2{margin:0;font-size:16px;font-weight:700}.profile-rosgan-lots p{font-size:12px;line-height:1.5;color:#627b94;margin:4px 0}.profile-rosgan-lots button,.profile-lot-actions a{font:inherit;font-size:12px;font-weight:600;border:1px solid #dce5ee;border-radius:7px;background:#fff;color:#002e5f;padding:6px 9px;cursor:pointer;text-decoration:none}
2354.profile-rosgan-row{display:grid;grid-template-columns:minmax(160px,1.2fr) 64px minmax(150px,1fr) auto;gap:16px;align-items:center;padding:11px 0;border-top:1px solid #edf1f5}.profile-rosgan-row h3{margin:2px 0;font-size:14px;font-weight:600}.profile-rosgan-row>div>span{display:block;color:#627b94;font-size:11px;margin-top:2px}.profile-rosgan-row>div>strong{font-size:12px;font-weight:600}.profile-rosgan-row>div:nth-child(2)>strong{font-size:16px}.profile-lot-status{text-transform:capitalize}.profile-rosgan-row .profile-lot-own{color:#007c66;font-weight:600}.profile-lot-actions{display:flex;gap:6px;flex-wrap:wrap}
2355@media(max-width:650px){.profile-company{padding:10px 12px;gap:12px}.profile-company h2{font-size:14px}.profile-rosgan-lots{padding:12px}.profile-rosgan-row{grid-template-columns:minmax(0,1fr) 64px;gap:8px 12px}.profile-rosgan-lots header{align-items:flex-start}.profile-lot-actions{justify-content:flex-end}}
2356.profile-area,.profile-hero,.profile-actions,.profile-action,.profile-kpi-grid,.profile-kpi-card{box-sizing:border-box}
2357.lot-wizard-page,.lot-wizard-page *{box-sizing:border-box}
2358.lot-wizard-page,.lot-wizard-header,.lot-wizard-grid,.lot-form-panel,.lot-fields-grid,.establishment-form-card,.establishment-form-grid{min-width:0;max-width:100%}
2359.lot-wizard-page input:not([type="checkbox"]):not([type="radio"]),.lot-wizard-page select,.lot-wizard-page textarea{width:100%;min-width:0;max-width:100%}
2360.establishment-cuit-field{grid-column:1/-1}
2361@media(max-width:700px){
2362  .lot-wizard-page{width:100%;max-width:100vw!important;padding:0 14px calc(104px + env(safe-area-inset-bottom))!important;overflow-x:clip}
2363  .lot-wizard-header{align-items:flex-start!important;padding:18px 0 15px!important}
2364  .lot-wizard-header>div{flex:1 1 100%;min-width:0}
2365  .lot-wizard-header>span:last-child{margin-left:auto}
2366  .lot-wizard-steps{gap:5px!important;margin-bottom:16px!important}
2367  .lot-wizard-steps>sc-for{display:contents}
2368  .lot-wizard-step-copy{display:none!important}
2369  .lot-wizard-grid{grid-template-columns:minmax(0,1fr)!important;gap:14px!important}
2370  .lot-form-panel{width:100%;padding:20px!important;border-radius:18px!important}
2371  .lot-fields-grid,.establishment-form-grid,.lot-three-fields-grid{grid-template-columns:minmax(0,1fr)!important}
2372  .establishment-form-card{padding:16px!important}
2373  .establishment-cuit-field{grid-column:auto}
2374  .lot-wizard-footer button{min-width:0;padding:11px 16px!important}
2375  @supports not (overflow:clip){.lot-wizard-page{overflow-x:hidden}}
2376}
2377.profile-actions{flex-wrap:wrap;max-width:100%}
2378.profile-action.is-selected,.profile-action[aria-pressed="true"]{background:#fff!important;color:#07396a!important;border-color:#fff!important;box-shadow:0 0 0 3px rgba(126,205,255,.45),0 4px 12px #001f4033}
2379.profile-action.is-selected img,.profile-action[aria-pressed="true"] img{filter:brightness(0) saturate(100%) invert(17%) sepia(42%) saturate(1860%) hue-rotate(179deg)!important}
2380.profile-action:focus-visible{outline:3px solid #8bd8ff;outline-offset:3px}
2381.profile-actions>sc-if,.profile-kpi-grid>sc-for{display:contents}
2382
2383.feria-control-actions{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin:0 0 20px}
2384.feria-control-button{display:inline-flex;align-items:center;justify-content:center;gap:9px;box-sizing:border-box;height:46px;min-height:46px;margin:0;padding:0 16px;background:#fff;border:1px solid #dce6f0;border-radius:12px;color:#002e5f;font-family:inherit;font-size:13px;font-weight:600;line-height:20px;cursor:pointer}
2385.feria-control-button img{display:block;flex:none;object-fit:contain}
2386.feria-control-button.is-youtube{background:#002e5f;border-color:#002e5f;color:#fff}
2387.feria-control-button:hover{background:#edf4fa;border-color:#a9c4df}
2388.feria-control-button.is-youtube:hover{background:#074a88;border-color:#074a88}
2389.feria-control-button:focus-visible{outline:3px solid #79b9f1;outline-offset:3px}
2390@media(max-width:520px){.feria-control-button{width:100%}}
2391.profile-feria-actions{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px;margin-top:18px;padding-top:16px;border-top:1px solid #e7edf4}
2392.profile-feria-share-actions,.profile-feria-manage-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
2393.profile-feria-card .profile-feria-action{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;margin:0;padding:9px 13px;border:1px solid #d4e1ee;border-radius:10px;background:#f6f9fc;color:#174a73;font:700 12px/1.25 inherit;text-decoration:none;cursor:pointer;box-sizing:border-box;transition:background .15s,border-color .15s}
2394.profile-feria-action img{display:block;flex:none;width:16px;height:16px}
2395.profile-feria-card .profile-feria-action:hover{background:#eaf2fa;border-color:#a9c4df}
2396.profile-feria-card .profile-feria-action.is-primary{background:#075ca8;border-color:#075ca8;color:#fff}.profile-feria-action.is-primary img{filter:brightness(0) invert(1)}
2397.profile-feria-card .profile-feria-action.is-management{background:#fff}
2398.profile-feria-card .profile-feria-action.is-danger{background:#fff;color:#b42335;border-color:#f0d8dc}.profile-feria-action.is-danger img{filter:invert(22%) sepia(70%) saturate(2100%) hue-rotate(330deg)}
2399.profile-feria-card .profile-feria-action:focus-visible{outline:3px solid #8ac4ff;outline-offset:2px}.profile-feria-card .profile-feria-action:disabled{opacity:.55;cursor:wait}
2400@media(max-width:600px){.profile-feria-share-actions{width:100%}.profile-feria-share-actions .profile-feria-action{flex:1 1 auto}.profile-feria-manage-actions{width:100%;justify-content:flex-end}}
2401
2402.profile-feria-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr));gap:12px}.profile-feria-card{padding:16px;border:1px solid #dce7f0;border-radius:14px;background:#fff;min-width:0}.profile-feria-brand{display:flex;align-items:center;gap:8px;font-size:11px;color:#65758a}.profile-feria-brand img{width:32px;height:32px;object-fit:contain}.profile-feria-brand strong{margin-left:auto;color:#087664;background:#e5f5ef;padding:4px 7px;border-radius:6px;white-space:nowrap}.profile-feria-card h3{margin:12px 0 8px;font-size:16px;color:#112f4b}.profile-feria-card p{font-size:12px;color:#65758a;margin:5px 0}.profile-feria-card a{display:inline-block;margin-top:12px;font-size:12px;font-weight:700;color:#075ca8}.profile-feria-card small{display:block;margin-top:12px;color:#718397}
2403.lot-loading-switch{display:inline-flex;align-items:center;gap:8px}.lot-loading-track{width:30px;height:18px;border-radius:20px;background:#8797a8;display:inline-block;position:relative;flex:none}.lot-loading-track:after{content:"";position:absolute;width:12px;height:12px;top:3px;left:3px;background:white;border-radius:50%;transition:transform .15s}.lot-loading-switch[aria-checked="true"] .lot-loading-track{background:#087f65}.lot-loading-switch[aria-checked="true"] .lot-loading-track:after{transform:translateX(12px)}
2404.profile-email{min-width:0;max-width:100%;overflow-wrap:anywhere}
2405.ba-admin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,440px),1fr));gap:16px;align-items:start}
2406.ba-admin-card{min-width:0;background:#fff;border-radius:16px;box-shadow:0 8px 26px rgba(0,30,60,.07);padding:16px 18px;display:flex;flex-direction:column;gap:14px}
2407.ba-admin-editor{min-width:0;display:grid;grid-template-columns:minmax(125px,.9fr) minmax(150px,1.15fr) auto;gap:10px;align-items:end}
2408.ba-admin-field{min-width:0;display:flex;flex-direction:column;gap:5px}
2409.ba-admin-field>span{font-size:10.5px;letter-spacing:.05em;color:#8593a6;font-weight:700;text-transform:uppercase}
2410.ba-admin-field>input{box-sizing:border-box;width:100%;min-width:0;height:38px;border:1px solid #dde7f2;border-radius:10px;padding:9px 10px;font:inherit;font-size:13px;color:#112033}
2411.ba-admin-save{box-sizing:border-box;height:38px;background:#0a5ca8;color:#fff;border:0;border-radius:9px;padding:0 13px;font:inherit;font-size:12px;font-weight:800;white-space:nowrap;cursor:pointer}
2412.ba-admin-save:disabled{cursor:wait;opacity:.65}
2413.credit-live-panel{background:#fff;border-radius:18px;box-shadow:0 8px 26px rgba(0,30,60,.07);overflow:hidden;margin-bottom:16px}
2414.credit-live-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:15px 20px;border-bottom:1px solid #edf2f7}
2415.credit-live-title{display:flex;align-items:center;gap:10px;min-width:0}.credit-live-title h3{margin:2px 0 0;color:#112033;font-size:18px;line-height:1.1}.credit-live-eyebrow{color:#8593a6;font-size:10px;font-weight:900;letter-spacing:.12em}
2416.credit-live-status{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;color:#19755d;font-size:9.5px;font-weight:900;letter-spacing:.08em}.credit-live-status:before{content:"";width:7px;height:7px;border-radius:50%;background:#20c997;box-shadow:0 0 0 5px rgba(32,201,151,.12);animation:creditLivePulse 1.8s ease-in-out infinite}
2417.credit-live-history{min-height:34px;display:inline-flex;align-items:center;gap:6px;border:1px solid #c8d9e8;border-radius:9px;background:#fff;color:#075ca8;padding:7px 10px;font:800 10.5px inherit;cursor:pointer}
2418.credit-live-row{padding:13px 20px 15px;border-bottom:1px solid #edf2f7}.credit-live-row:last-child{border-bottom:0}
2419.credit-live-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.credit-live-party{min-width:0}.credit-live-party strong{display:block;color:#112033;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2419.credit-live-party span{display:block;margin-top:2px;color:#7a8b9d;font-size:10.5px}
2420.credit-live-metrics{display:grid;grid-template-columns:minmax(150px,1.2fr) repeat(2,minmax(120px,.75fr));gap:8px}.credit-live-metric{min-width:0;padding:10px 12px;border:1px solid #e1eaf2;border-radius:11px;background:#f8fbfd}.credit-live-metric small{display:block;color:#718397;font-size:8.5px;font-weight:900;letter-spacing:.08em}.credit-live-metric strong{display:block;margin-top:4px;color:#12314f;font-size:16px;font-variant-numeric:tabular-nums;white-space:nowrap}.credit-live-metric.is-primary{background:linear-gradient(135deg,#edf9f4,#f6fcf9);border-color:#cbe8dc}.credit-live-metric.is-primary strong{color:#087a5c;font-size:20px}.credit-live-metric.is-used strong{color:#9b6513}
2421.credit-live-progress{height:5px;margin-top:8px;border-radius:999px;background:#dde7f0;overflow:hidden}.credit-live-progress>span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#0a5ca8,#20a37d);transition:width .55s cubic-bezier(.22,.72,.18,1)}
2422.credit-live-movements{display:flex;gap:7px;margin-top:9px;overflow:hidden}.credit-live-movement{min-width:0;flex:1;display:flex;align-items:center;justify-content:space-between;gap:7px;padding:8px 10px;border:1px solid #e3ebf3;border-radius:9px;background:#fff}.credit-live-movement>div{min-width:0}.credit-live-movement strong{display:block;color:#163651;font-size:10.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.credit-live-movement small{display:block;margin-top:2px;color:#7a8c9e;font-size:8.5px}.credit-live-movement b{color:#075ca8;font-size:10.5px;white-space:nowrap}
2423@keyframes creditLivePulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.48;transform:scale(.72)}}
2424.launch-admin-list{display:grid;gap:8px;max-height:420px;overflow:auto;padding-right:3px}
2425.launch-admin-row{display:grid;grid-template-columns:minmax(170px,1.2fr) minmax(210px,1.3fr) minmax(145px,.85fr) auto;gap:12px;align-items:center;padding:11px 13px;border:1px solid #e1eaf2;border-radius:11px;background:#f8fbfe}
2426.launch-admin-row>*{min-width:0}.launch-admin-row strong,.launch-admin-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.launch-admin-row strong{font-size:12.5px;color:#112033}.launch-admin-row span{font-size:11.5px;color:#65758a}.launch-admin-channels{display:flex;gap:5px;justify-content:flex-end}.launch-admin-channel{padding:4px 7px;border-radius:999px;background:#e8f5f0;color:#176c55;font-size:9.5px;font-weight:850;white-space:nowrap}
2427@media(max-width:640px){.ba-admin-card{padding:14px 16px}.ba-admin-editor{grid-template-columns:minmax(0,1fr) minmax(0,1.15fr)}.ba-admin-save{grid-column:1/-1;width:100%}}
2428@media(max-width:640px){.credit-live-head{padding:13px 14px}.credit-live-row{padding:12px 14px 14px}.credit-live-metrics{grid-template-columns:1fr 1fr}.credit-live-metric.is-primary{grid-column:1/-1}.credit-live-metric strong{font-size:14px}.credit-live-metric.is-primary strong{font-size:19px}.credit-live-movements{flex-direction:column}.credit-live-row-head{align-items:flex-start}.credit-live-history{padding:6px 8px}.credit-live-status{font-size:8.5px}}
2429@media(max-width:760px){.launch-admin-row{grid-template-columns:minmax(0,1fr) auto}.launch-admin-row .launch-admin-email,.launch-admin-row .launch-admin-phone{grid-column:1/-1}.launch-admin-channels{grid-column:2;grid-row:1;align-self:start}}
2430@media(max-width:360px){.ba-admin-editor{grid-template-columns:1fr}.ba-admin-save{grid-column:1}}
2431@media(max-width:1040px){.profile-actions{width:100%}}
2432@media(max-width:760px){
2433  .profile-area{width:100%;max-width:100vw!important;padding:0 14px calc(112px + env(safe-area-inset-bottom))!important;overflow-x:clip}
2434  .profile-area>*,.profile-area section,.profile-area article,.profile-area details,.profile-area [data-reveal],.profile-area #profile-analytics-grid{min-width:0;max-width:100%;box-sizing:border-box}
2435  .profile-area [style*="display:grid"],.profile-area [style*="display:flex"]{min-width:0;max-width:100%}
2436  .profile-area input:not([type="checkbox"]):not([type="radio"]),.profile-area select,.profile-area textarea,.profile-area button{min-width:0;max-width:100%;box-sizing:border-box}
2437  .profile-area [style*="min-width:130px"],.profile-area [style*="min-width:140px"],.profile-area [style*="min-width:150px"],.profile-area [style*="min-width:170px"],.profile-area [style*="min-width:180px"],.profile-area [style*="min-width:210px"]{min-width:0!important;overflow-wrap:anywhere}
2438  .profile-area .buyer-credit-panel,.profile-area .buyer-credit-row,.profile-area .ba-admin-grid,.profile-area .ba-admin-card,.profile-area .ba-admin-editor,.profile-area .launch-admin-row{width:100%;min-width:0;max-width:100%;box-sizing:border-box}
2439  .profile-area .profile-panel-head,.profile-area .commercial-report-head{flex-wrap:wrap}
2440  @supports not (overflow:clip){.profile-area{overflow-x:hidden}}
2441  .profile-hero{display:grid!important;grid-template-columns:64px minmax(0,1fr);align-items:start!important;gap:13px!important;margin:14px 0!important;padding:18px!important;border-radius:20px!important}
2442  .profile-hero-avatar{grid-column:1;grid-row:1;width:64px!important;height:64px!important}
2443  .profile-identity{grid-column:2;grid-row:1;min-width:0!important;width:100%}
2444  .profile-kicker{font-size:9px!important;line-height:1.3;letter-spacing:.11em!important}
2445  .profile-identity h1{font-size:22px!important;line-height:1.05;margin-top:4px!important;overflow-wrap:anywhere}
2446  .profile-email{font-size:11.5px!important;line-height:1.3}
2447  .profile-email img{flex:0 0 auto}
2448  .profile-actions{grid-column:1/-1;grid-row:2;display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px!important;width:100%;margin-top:3px}
2449  .profile-action{width:100%;min-width:0;min-height:48px;padding:9px 8px!important;display:flex!important;align-items:center!important;justify-content:center!important;gap:6px!important;border-radius:11px!important;font-size:11.5px!important;font-weight:750!important;line-height:1.15;text-align:center;white-space:normal}
2450  .profile-action img{width:15px!important;height:15px!important;flex:0 0 auto}
2451  .profile-action-load,.profile-action-logout{font-size:12px!important}
2452  .profile-kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:10px!important;margin-bottom:14px!important}
2453  .profile-kpi-card{min-width:0;min-height:124px;padding:15px!important;border-radius:16px!important}
2454  .profile-editor-overlay{place-items:end center!important;padding:0!important}
2455  .profile-editor-dialog{width:100%!important;max-height:calc(100dvh - 14px);overflow:auto!important;border-radius:22px 22px 0 0!important}
2456  .profile-editor-footer{position:sticky;bottom:0;z-index:2;padding:13px 16px calc(13px + env(safe-area-inset-bottom))!important}
2457  .profile-editor-footer button{flex:1;min-width:0}
2458}
2459@media(max-width:350px){.profile-kpi-grid,.profile-actions{grid-template-columns:1fr!important}}
2460html[data-public-advance="lacassina"] body{background:#f4f7fb}
2461html[data-public-advance="lacassina"] .advance-public-hidden{display:none!important}
2462html[data-public-advance="lacassina"] [data-public-blocked-overlay]{display:none!important}
2463html[data-public-route="lacassina-stats"] sc-if:not([value="{{ isCabanaStats }}"]){display:none!important}
2464html[data-public-route="lacassina-stats"] sc-if[value="{{ isHome }}"],
2465html[data-public-route="lacassina-stats"] sc-if[value="{{ isPool }}"],
2466html[data-public-route="lacassina-stats"] sc-if[value="{{ isLot }}"],
2467html[data-public-route="lacassina-stats"] sc-if[value="{{ isLoad }}"],
2468html[data-public-route="lacassina-stats"] sc-if[value="{{ isFeria }}"],
2469html[data-public-route="lacassina-stats"] sc-if[value="{{ isProfile }}"],
2470html[data-public-route="lacassina-stats"] sc-if[value="{{ isNotifications }}"],
2471html[data-public-route="lacassina-stats"] sc-if[value="{{ isRemates }}"],
2472html[data-public-route="lacassina-stats"] sc-if[value="{{ isRemateDetail }}"],
2473html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanas }}"],
2474html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaDetail }}"],
2475html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaLotDetail }}"],
2476html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaLotCreate }}"],
2477html[data-public-route="lacassina-stats"] sc-if[value="{{ isRemateConfig }}"],
2478html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaSalesOrder }}"],
2479html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaCommercial }}"],
2480html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaPriceBoard }}"],
2481html[data-public-route="lacassina-stats"] sc-if[value="{{ isChannel }}"],
2482html[data-public-route="lacassina-stats"] sc-if[value="{{ showStatistics }}"]{display:none!important}
2483html[data-public-route="cabana-tv"]{position:fixed;inset:0;width:100vw;height:100vh;min-width:0;min-height:0;max-width:100vw;max-height:100vh;overflow:hidden!important;overflow:clip!important;overscroll-behavior:none;background:transparent!important;scrollbar-width:none;touch-action:none}
2484html[data-public-route="cabana-tv"] *,html[data-public-route="cabana-tv"] *:before,html[data-public-route="cabana-tv"] *:after{box-sizing:border-box}
2485html[data-public-route="cabana-tv"]::-webkit-scrollbar,html[data-public-route="cabana-tv"] body::-webkit-scrollbar,html[data-public-route="cabana-tv"] x-dc::-webkit-scrollbar,html[data-public-route="cabana-tv"] #dc-root::-webkit-scrollbar,html[data-public-route="cabana-tv"] .sc-host::-webkit-scrollbar{display:none;width:0;height:0}
2486html[data-public-route="cabana-tv"] body{position:fixed;inset:0;width:100vw;height:100vh;min-width:0;min-height:0;max-width:100vw;max-height:100vh;margin:0;background:transparent!important;overflow:hidden!important;overflow:clip!important;overscroll-behavior:none;scrollbar-width:none;touch-action:none}
2487html[data-public-route="cabana-tv"] x-dc,html[data-public-route="cabana-tv"] #dc-root,html[data-public-route="cabana-tv"] .sc-host,html[data-public-route="cabana-tv"] .sc-host>div{position:relative;width:100vw!important;height:100vh!important;min-width:0!important;min-height:0!important;max-width:100vw!important;max-height:100vh!important;margin:0!important;background:transparent!important;overflow:hidden!important;overflow:clip!important;overscroll-behavior:none;scrollbar-width:none}
2488html[data-public-route="cabana-tv-config"] body{margin:0;background:#eef4fa}
2489.tv-config-category-fields{display:grid;gap:10px;max-height:640px;overflow:auto;padding-right:4px}.tv-config-category-card{border:1px solid #dce7f2;border-radius:10px;background:#fff;padding:10px}.tv-config-category-card h3{margin:0 0 8px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#0a5ca8}.tv-config-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.tv-config-card-head h2{margin:0}.tv-config-mini-toggle{display:flex;align-items:center;gap:9px;border:1px solid #dce7f2;border-radius:999px;background:#fff;padding:6px 7px 6px 11px;color:#173a5d;font-size:11px;font-weight:950}.tv-config-mini-toggle .tv-config-switch{width:45px;height:25px;padding:3px}.tv-config-mini-toggle .tv-config-switch span{width:19px;height:19px}.tv-config-mini-toggle .tv-config-switch.is-on span{transform:translateX(20px)}.tv-config-subforms{display:grid;gap:8px}.tv-config-subform{border:1px solid #e4edf6;border-radius:9px;background:#f8fbfe;padding:8px}.tv-config-subform h4{margin:0 0 7px;color:#65758a;font-size:10px;letter-spacing:.12em;text-transform:uppercase}.tv-config-category-card .tv-config-checks{grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:7px}.tv-config-category-card .tv-config-check{border-radius:9px;padding:8px 9px}
2490.tv-lower-page{position:fixed;inset:0;z-index:2;width:100vw;height:100vh;min-width:0;min-height:0;max-width:100vw;max-height:100vh;background:transparent;color:#fff;font-family:'Archivo',Arial,sans-serif;overflow:hidden;overflow:clip;isolation:isolate;contain:layout paint size}
2491.tv-lower-stage{position:absolute;inset:0;width:100%;height:100%;min-width:0;min-height:0;max-width:100%;max-height:100%;background:transparent;overflow:hidden;overflow:clip;isolation:isolate;contain:layout paint size}
2492.tv-lower-control{position:absolute;top:18px;left:18px;z-index:10;display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid rgba(255,255,255,.22);border-radius:999px;background:rgba(4,25,48,.72);backdrop-filter:blur(14px);box-shadow:0 12px 30px rgba(0,0,0,.22)}
2493.tv-lower-control button,.tv-lower-control select{height:34px;border:1px solid rgba(255,255,255,.22);border-radius:999px;background:rgba(255,255,255,.1);color:#fff;font:900 12px 'Archivo',sans-serif;padding:0 12px;outline:0}.tv-lower-control select{min-width:190px}.tv-lower-control option{color:#082f5c}
2493.tv-lower-control button{cursor:pointer}
2494.tv-lower-third{position:absolute;left:2.6vw;right:2.6vw;bottom:4.4vh;height:clamp(136px,23.5vh,254px);min-width:0;min-height:0;max-height:calc(100vh - 8.8vh);display:grid;grid-template-columns:clamp(164px,13vw,260px) minmax(0,1fr);grid-template-rows:clamp(34px,4.45vh,48px) minmax(0,1fr);align-items:stretch;filter:drop-shadow(0 24px 42px rgba(0,0,0,.36));z-index:3;overflow:visible}
2495.tv-lower-third:before{display:none}
2496.tv-lower-topline{grid-column:1 / -1;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:16px;min-width:0;padding:0 20px 0 22px;border-radius:14px 14px 0 0;background:linear-gradient(90deg,rgba(5,19,34,.92),rgba(0,46,95,.88) 58%,rgba(34,180,189,.76));border:1px solid rgba(255,255,255,.18);border-bottom:0;box-shadow:inset 0 1px rgba(255,255,255,.12)}
2497.tv-lower-topline:before{content:"";position:absolute;left:22px;right:22px;bottom:0;height:1px;background:linear-gradient(90deg,#22b4bd,rgba(255,255,255,.34),transparent)}
2498.tv-lower-title-line{min-width:0;display:flex;align-items:center;gap:13px}.tv-lower-title-line:before{content:"";width:9px;height:9px;border-radius:50%;background:#22b4bd;box-shadow:0 0 18px rgba(34,180,189,.88);flex:0 0 9px}
2499.tv-lower-title-line strong{min-width:0;color:#fff;font-size:clamp(14px,1.22vw,22px);font-weight:950;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase;display:flex;align-items:center;gap:11px}.tv-title-count{display:inline-flex;align-items:center;gap:11px;color:#fff;font:inherit;letter-spacing:0}.tv-title-count b{font:inherit;color:#fff}.tv-title-dot{color:#22b4bd;font:inherit;line-height:1}.tv-deps-pill{display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:999px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22);color:#9eeaf0;font-size:clamp(9px,.62vw,12px);font-weight:950;letter-spacing:.08em}
2500.tv-lower-meta{display:flex;align-items:center;justify-content:flex-end;gap:10px;min-width:0;color:rgba(255,255,255,.88);font-size:clamp(10px,.78vw,13px);font-weight:950;letter-spacing:.02em;white-space:nowrap}.tv-channel-bug{position:absolute;top:4.2vh;left:3vw;z-index:4;display:grid;gap:9px;justify-items:start;width:clamp(520px,43vw,740px);filter:drop-shadow(0 18px 34px rgba(0,0,0,.28))}.tv-bug-strip{height:62px;display:grid;grid-template-columns:.95fr 1.2fr 1.16fr;align-items:stretch;overflow:hidden;border-radius:0 999px 999px 0;border:1px solid rgba(255,255,255,.3);background:rgba(255,255,255,.1);box-shadow:inset 0 1px rgba(255,255,255,.18)}.tv-bug-part{min-width:0;display:flex;align-items:center;justify-content:center;gap:10px;padding:0 18px;border-left:1px solid rgba(255,255,255,.18);color:#fff;overflow:hidden}.tv-bug-part:first-child{border-left:0}.tv-bug-rosgan{background:#fff;justify-content:flex-start;padding-left:22px}.tv-bug-cons{background:var(--tv-cons-color,#d91549)}.tv-bug-cabana{background:var(--tv-cabana-color,#0a5ca8)}.tv-bug-logo{max-width:100%;max-height:40px;object-fit:contain;display:block}.tv-bug-rosgan .tv-bug-logo{max-width:158px;max-height:39px}.tv-bug-cons .tv-bug-logo{width:39px;height:39px;object-fit:contain;filter:brightness(0) invert(1);flex:0 0 39px}.tv-bug-cabana .tv-bug-logo{max-width:190px;max-height:42px;filter:var(--tv-cabana-logo-filter,none)}.tv-bug-cons strong,.tv-bug-cabana strong{font-size:14px;font-weight:950;line-height:1;overflow:hidden;text-overflow:ellipsis}.tv-bug-cons-name{min-width:0;max-width:190px;overflow:hidden;text-overflow:ellipsis;font-size:15px;font-weight:950}.tv-bug-live{min-height:38px;display:flex;align-items:center;justify-content:flex-start;gap:9px;padding-left:0;color:#fff;font-size:16px;font-weight:950;letter-spacing:.02em;line-height:1;max-width:100%}.tv-bug-clock,.tv-bug-live-card{display:inline-flex;align-items:center;border:1px solid rgba(255,255,255,.24);backdrop-filter:blur(10px);box-shadow:0 12px 24px rgba(2,16,31,.18)}.tv-bug-clock{min-height:36px;padding:0 14px 0 16px;gap:8px;border-radius:0 999px 999px 0;background:rgba(4,22,39,.76);font-size:16px;letter-spacing:.03em;color:#fff}.tv-bug-clock:before{content:"";width:15px;height:15px;border:2px solid rgba(255,255,255,.92);border-radius:50%;box-sizing:border-box;box-shadow:inset 4px -4px 0 -3px rgba(255,255,255,.92);flex:0 0 15px}.tv-bug-live-card{min-height:36px;min-width:0;max-width:560px;padding:0 16px;gap:10px;border-radius:999px;background:linear-gradient(90deg,rgba(0,46,95,.78),rgba(34,180,189,.54));color:#fff}.tv-bug-live-card b{display:inline-flex;align-items:center;gap:8px;color:#fff;text-transform:uppercase;font-size:15px;flex:0 0 auto}.tv-bug-live-card b:before{content:"";width:8px;height:8px;border-radius:50%;background:#ef4444;box-shadow:0 0 13px rgba(239,68,68,.95)}.tv-bug-live-card span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgba(255,255,255,.92);font-size:16px}
2501.tv-lower-lot-panel{grid-row:2;display:grid;grid-template-rows:auto 1fr;align-items:center;min-width:0;padding:16px 20px;border-radius:0 0 0 14px;background:linear-gradient(140deg,#002e5f,#075c9d 72%,#22b4bd);box-shadow:inset -1px 0 rgba(255,255,255,.16);overflow:hidden}
2502.tv-lower-logo{height:40px;
2502display:flex;align-items:center;justify-content:center}.tv-lower-logo img{max-width:136px;max-height:38px;object-fit:contain;filter:brightness(0) invert(1)}.tv-lower-logo strong{max-width:100%;font-size:clamp(13px,1.1vw,18px);font-weight:950;line-height:1.05;text-align:center;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
2503.tv-lower-lot{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;font-size:clamp(28px,2.5vw,46px);font-weight:950;line-height:.95;white-space:nowrap;text-align:center}.tv-lower-lot small{font-size:clamp(13px,1.05vw,20px);font-weight:950;line-height:1;color:rgba(255,255,255,.88);letter-spacing:.04em}.tv-lower-rps{display:none}
2504.tv-lower-table{grid-row:2;min-width:0;min-height:0;display:grid;grid-template-rows:clamp(25px,3.34vh,36px);grid-auto-rows:minmax(0,1fr);border-radius:0 0 14px 0;overflow:hidden;background:rgba(28,26,28,.93);border:1px solid rgba(255,255,255,.16);border-left:0;box-shadow:0 18px 45px rgba(0,0,0,.22)}
2505.tv-lower-row{display:grid;grid-template-columns:var(--tv-cols,1.15fr .72fr .52fr .62fr 1.2fr .78fr);min-width:0}.tv-lower-row>*{min-width:0;display:flex;align-items:center;justify-content:center;padding:3px 9px;border-left:1px solid rgba(255,255,255,.12);border-bottom:1px solid rgba(255,255,255,.12);font-size:clamp(15px,1.08vw,21px);font-weight:900;line-height:1.08;white-space:normal;overflow:hidden;text-align:center;text-overflow:clip;overflow-wrap:anywhere}.tv-lower-row>* > i{display:flex;align-items:center;justify-content:center;width:100%;max-width:100%;max-height:3.24em;line-height:1.08;font-style:italic;white-space:normal;overflow:hidden;overflow-wrap:anywhere;text-align:center}.tv-lower-row>*:first-child{border-left:0}.tv-lower-head>*{padding:2px 8px;background:rgba(255,255,255,.055);font-size:clamp(13px,1vw,19px);font-weight:950;color:#fff;letter-spacing:.02em}.tv-lower-body>*{padding:5px 9px}.tv-lower-body>*.is-long{font-size:clamp(11px,.9vw,16px)}.tv-lower-body>*.is-very-long{font-size:clamp(9.5px,.76vw,13px)}.tv-lower-body:nth-child(odd)>*{background:rgba(255,255,255,.045)}
2506.tv-lower-row.is-sold>*{background:linear-gradient(90deg,rgba(0,145,96,.34),rgba(0,145,96,.16) 42%,rgba(255,255,255,.04))!important;box-shadow:inset 0 1px rgba(31,220,150,.24),inset 0 -1px rgba(31,220,150,.2);color:#fff}.tv-lower-row.is-lifted>*{position:relative;background:linear-gradient(90deg,rgba(94,103,115,.42),rgba(94,103,115,.19) 42%,rgba(255,255,255,.025))!important;box-shadow:inset 0 1px rgba(203,213,225,.22),inset 0 -1px rgba(203,213,225,.16);color:rgba(255,255,255,.48);filter:saturate(.55)}.tv-lower-row.is-lifted>* > i{text-decoration:line-through;text-decoration-thickness:2px;text-decoration-color:rgba(226,232,240,.72);text-underline-offset:0}.tv-lower-row.is-sold>*:first-child,.tv-lower-row.is-lifted>*:first-child{position:relative}.tv-lower-row.is-sold>*:first-child:before,.tv-lower-row.is-lifted>*:first-child:before{content:"";position:absolute;left:0;top:0;bottom:0;width:7px}.tv-lower-row.is-sold>*:first-child:before{background:#12b981;box-shadow:0 0 16px rgba(18,185,129,.68)}.tv-lower-row.is-lifted>*:first-child:before{background:#94a3b8;box-shadow:0 0 14px rgba(148,163,184,.42)}
2507.tv-lower-sale-cell{padding-left:8px!important;padding-right:8px!important;text-align:center}.tv-lower-sale-cell .tv-roll{width:100%;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;font-size:clamp(13px,.98vw,18px);font-style:normal;text-transform:uppercase;letter-spacing:.01em;overflow:visible;text-overflow:clip;white-space:normal;text-decoration:none!important}.tv-lower-sale-cell.is-sold{background:rgba(0,145,96,.96)!important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12),0 0 18px rgba(0,145,96,.18)}.tv-lower-sale-cell.is-lifted{background:rgba(100,116,139,.9)!important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.1),0 0 14px rgba(100,116,139,.16)}.tv-lower-sale-cell.is-empty{background:rgba(255,255,255,.05)!important;color:rgba(255,255,255,.68)}.tv-lower-row.is-lifted .tv-lower-sale-cell .tv-roll{text-decoration:none!important;color:#fff}
2508.tv-lower-table.is-many .tv-lower-row>*{font-size:clamp(14px,1.02vw,20px);padding:0 8px}.tv-lower-table.is-many .tv-lower-head>*{font-size:clamp(11px,.84vw,16px)}.tv-lower-table.is-many .tv-lower-body>*.is-long{font-size:clamp(11px,.9vw,16px)}.tv-lower-table.is-many .tv-lower-body>*.is-very-long{font-size:clamp(9.5px,.76vw,13px)}.tv-lower-table.is-max .tv-lower-row>*{font-size:clamp(12px,.82vw,16px);padding:0 6px}.tv-lower-table.is-max .tv-lower-head>*{font-size:clamp(10px,.7vw,14px)}.tv-lower-table.is-max .tv-lower-body>*.is-long{font-size:clamp(10px,.76vw,13px)}.tv-lower-table.is-max .tv-lower-body>*.is-very-long{font-size:clamp(8.5px,.68vw,11.5px)}
2509.tv-lower-table.is-deps .tv-lower-row>*{font-size:clamp(15px,1.08vw,21px);font-style:normal}.tv-lower-table.is-deps.is-single-dep{grid-template-rows:repeat(3,minmax(0,1fr));grid-auto-rows:minmax(0,1fr)}.tv-lower-table.is-deps.is-single-dep .tv-lower-body>*:first-child{justify-content:center;padding-left:6px;padding-right:6px;color:#9eeaf0;text-transform:uppercase;letter-spacing:.04em;font-size:clamp(12px,.82vw,16px)}.tv-lower-table.is-deps.is-single-dep .tv-lower-body:first-of-type>*{background:rgba(255,255,255,.055);color:#fff;text-transform:uppercase;letter-spacing:.03em}.tv-lower-table.is-deps.is-single-dep .tv-lower-row>*{font-size:clamp(14px,1.02vw,20px);padding-left:9px;padding-right:9px}.tv-lower-table.is-deps.is-single-dep .tv-lower-body>* > i{font-style:normal}
2510.tv-lower-empty{position:absolute;
2510left:50%;bottom:8vh;transform:translateX(-50%);padding:18px 22px;border-radius:16px;background:rgba(0,46,95,.86);font-weight:900}
2511.tv-config-page{min-height:100vh;background:#eef4fa;color:#082033;font-family:'Archivo',Arial,sans-serif;padding:28px 22px 60px;box-sizing:border-box}.tv-config-shell{max-width:1180px;margin:0 auto}.tv-config-back{border:0;background:transparent;color:#0a5ca8;font:900 13px 'Archivo',sans-serif;cursor:pointer;padding:0;margin-bottom:14px}.tv-config-hero{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;padding:22px;border-radius:16px;background:linear-gradient(135deg,#002e5f,#0a5ca8 64%,#22b4bd);color:#fff;box-shadow:0 18px 42px rgba(0,46,95,.16)}.tv-config-hero small{font-size:11px;font-weight:950;letter-spacing:.16em;text-transform:uppercase;color:#9eeaf0}.tv-config-hero h1{margin:6px 0 8px;font-size:clamp(30px,4vw,56px);line-height:.96}.tv-config-hero p{margin:0;max-width:720px;color:rgba(255,255,255,.86);font-size:14px}.tv-config-live{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:999px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);font-weight:950}.tv-config-switch{width:58px;height:32px;border:0;border-radius:999px;background:#94a3b8;padding:4px;cursor:pointer;transition:background .2s}.tv-config-switch span{display:block;width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 4px 10px rgba(0,0,0,.18);transition:transform .2s}.tv-config-switch.is-on{background:#12b981}.tv-config-switch.is-on span{transform:translateX(26px)}.tv-config-panel{margin-top:16px;border:1px solid #d8e5f1;border-radius:14px;background:#fff;box-shadow:0 14px 36px rgba(15,42,70,.07);overflow:hidden}.tv-config-toolbar{display:grid;grid-template-columns:auto minmax(260px,1fr) auto;gap:10px;align-items:center;padding:14px;border-bottom:1px solid #e1eaf2}.tv-config-toolbar button,.tv-config-toolbar select{height:42px;border:1px solid #cfe0f1;border-radius:12px;background:#fff;color:#082f5c;font:900 13px 'Archivo',sans-serif;padding:0 13px}
2511.tv-config-toolbar button{cursor:pointer;background:#f5f9fd}.tv-config-toolbar select{width:100%}.tv-config-fields{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:16px;align-items:start}.tv-config-card{border:1px solid #dce7f2;border-radius:12px;background:#f8fbfe;padding:14px}.tv-config-card h2{margin:0 0 10px;font-size:16px}.tv-config-checks{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px}.tv-config-check{display:flex;align-items:center;gap:8px;border:1px solid #dbe7f2;border-radius:10px;background:#fff;padding:9px 10px;color:#173a5d;font-weight:900;font-size:12px}.tv-config-check input{accent-color:#0a5ca8}.tv-config-list{display:grid;gap:6px;max-height:640px;overflow:auto;padding-right:4px}.tv-config-lot{display:grid;grid-template-columns:54px minmax(0,1fr) 42px;gap:8px;align-items:center;border:1px solid #dce7f2;border-radius:9px;background:#fff;padding:7px 9px;cursor:pointer;min-height:40px}.tv-config-lot.is-active{border-color:#0a5ca8;box-shadow:0 0 0 2px rgba(10,92,168,.1)}.tv-config-lot strong{font-size:15px;color:#002e5f}.tv-config-lot span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:900;font-size:12.5px}.tv-config-lot small{color:#65758a;font-weight:900;font-size:11px;text-align:right}@media(max-width:760px){.tv-config-page{padding:18px 10px 40px}.tv-config-hero,.tv-config-toolbar,.tv-config-fields{grid-template-columns:1fr}.tv-config-live{justify-content:space-between}.tv-config-toolbar{gap:8px}.tv-config-fields{padding:10px}.tv-config-lot{grid-template-columns:50px minmax(0,1fr) 38px;padding:7px 8px}}
2512.tv-roll{position:relative;display:inline-block;max-width:100%;overflow:visible;text-overflow:ellipsis;white-space:nowrap;line-height:1.22;vertical-align:middle;backface-visibility:hidden;padding-right:.08em}.tv-roll.is-a{animation:tvWipeInA .42s cubic-bezier(.2,.72,.24,1) both}.tv-roll.is-b{animation:tvWipeInB .42s cubic-bezier(.2,.72,.24,1) both}.tv-lower-row .tv-roll{display:flex;align-items:center;justify-content:center}.tv-lower-row>*{overflow:hidden}.tv-lower-row>* .tv-roll{max-width:calc(100% - 2px);overflow:hidden;text-overflow:clip;white-space:normal;overflow-wrap:anywhere;text-align:center}.tv-lower-title-line .tv-roll,.tv-lower-lot .tv-roll{max-width:100%}
2513@keyframes tvWipeInA{from{opacity:.18;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@keyframes tvWipeInB{from{opacity:.18;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}
2514html[data-public-advance="lacassina"] sc-if[value="{{ profileCropOpen }}"],
2515html[data-public-advance="lacassina"] sc-if[value="{{ profileEditorOpen }}"],
2516html[data-public-advance="lacassina"] sc-if[value="{{ profilePhotoViewerOpen }}"],
2517html[data-public-advance="lacassina"] sc-if[value="{{ adminUsersOpen }}"],
2518html[data-public-advance="lacassina"] sc-if[value="{{ notificationsOpen }}"]{display:none!important}
2519.dc-lot-contact-card{background:#fff;border:1px solid #e1e9f1;border-radius:15px;padding:15px;box-shadow:0 12px 30px rgba(15,42,70,.08)}
2520.dc-cabana-catalog-card{display:flex;flex-direction:column;min-width:0}.dc-cabana-card-rp{max-width:calc(100% - 112px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dc-cabana-card-title{display:-webkit-box!important;overflow:hidden;line-height:1.12;-webkit-box-orient:vertical;-webkit-line-clamp:2}.dc-cabana-card-fact{min-width:0}.dc-cabana-card-fact strong{display:-webkit-box;overflow:hidden;line-height:1.22;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:anywhere}
2521.dc-prebid-side-card{position:relative;isolation:isolate;display:grid;gap:13px;overflow:hidden;padding:16px;border-radius:17px;background:linear-gradient(120deg,#002e5f,#0a5ca8,#087d79);background-size:190% 190%;color:#fff;box-shadow:0 16px 34px rgba(0,46,95,.2);animation:dcPrebidCardFlow 5s ease-in-out infinite}
2522.dc-prebid-side-card::after{content:"";position:absolute;z-index:0;inset:-55% auto -55% -45%;width:42%;transform:rotate(18deg) translateX(-220%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);animation:dcPrebidCardSweep 4.8s ease-in-out infinite;pointer-events:none}
2523.dc-prebid-side-head{position:relative;z-index:1;display:flex;align-items:center;gap:10px;min-width:0}
2524.dc-prebid-side-icon{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;border-radius:12px;background:rgba(255,255,255,.14)}
2525.dc-prebid-side-icon img{width:19px;height:19px;filter:brightness(0) invert(1)}
2526.dc-prebid-side-copy{min-width:0}.dc-prebid-side-copy small{display:flex;align-items:center;gap:6px;color:#9be6d0;font-size:9px;font-weight:900;letter-spacing:.12em}.dc-prebid-side-copy small::before{content:"";width:7px;height:7px;flex:0 0 7px;border-radius:50%;
2526background:#44e0a5;box-shadow:0 0 0 0 rgba(68,224,165,.6);animation:dcPrebidActivePulse 1.55s ease-out infinite}.dc-prebid-side-copy strong{display:block;margin-top:3px;font-size:15px;line-height:1.15}
2527.dc-prebid-side-actions{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr);align-items:stretch;gap:9px}
2528.dc-prebid-history-button{display:flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 12px;border:1px solid rgba(255,255,255,.35);border-radius:12px;background:transparent;color:#fff;font-family:inherit;font-size:12px;font-weight:700;cursor:pointer}
2529.dc-prebid-history-button:hover{background:rgba(255,255,255,.12)}
2530.dc-prebid-history-button:focus-visible{outline:2px solid #fff;outline-offset:2px}
2531.dc-prebid-side-amount{display:grid;align-content:center;gap:2px;min-width:0;padding:9px 11px;border:1px solid rgba(255,255,255,.2);border-radius:12px;background:rgba(255,255,255,.1)}
2532.dc-prebid-side-amount small{font-size:8px;font-weight:900;letter-spacing:.1em;opacity:.72}.dc-prebid-side-amount strong{overflow:hidden;font-size:17px;font-weight:950;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}
2533.dc-prebid-side-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:47px;border:0;border-radius:12px;padding:0 14px;background:#fff;color:#002e5f;cursor:pointer;font:900 12px inherit;box-shadow:0 9px 20px rgba(0,0,0,.15);transition:background-color .28s ease,color .28s ease,border-radius .32s ease,box-shadow .32s ease;animation:dcPrebidButtonBreathe 3.1s ease-in-out infinite}
2534.dc-prebid-side-button img{width:17px;height:17px}
2535.dc-prebid-side-button.is-position-transitioning{animation:none!important;will-change:transform,opacity}
2536main:has(.dc-prebid-side-card){animation:none!important;transform:none!important}
2537.dc-prebid-side-button.is-scroll-floating{position:fixed;z-index:56;top:76px;right:24px;bottom:auto;left:auto;width:auto;min-width:154px;min-height:50px;border:1px solid rgba(255,255,255,.36);border-radius:999px;padding:0 20px;background:linear-gradient(135deg,#087d55,#0aa36e);color:#fff;font-size:13px;box-shadow:0 17px 38px rgba(0,57,44,.3),0 0 0 5px rgba(18,185,129,.11);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);animation:dcPrebidMobileFloat 3.1s ease-in-out infinite}
2538.dc-prebid-side-button.is-scroll-floating img{width:20px;height:20px;filter:brightness(0) invert(1)}
2539@media(max-width:760px){
2540  .dc-prebid-side-button.is-scroll-floating{top:auto;right:max(14px,env(safe-area-inset-right));bottom:calc(76px + env(safe-area-inset-bottom));min-width:148px;padding:0 19px}
2541  .dc-prebid-side-button.is-scroll-floating.is-above-load-fab{bottom:calc(140px + env(safe-area-inset-bottom))}
2542}
2543@keyframes dcPrebidCardFlow{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
2544@keyframes dcPrebidCardSweep{0%,52%{transform:rotate(18deg) translateX(-220%);opacity:0}64%{opacity:1}82%,100%{transform:rotate(18deg) translateX(520%);opacity:0}}
2545@keyframes dcPrebidActivePulse{0%{box-shadow:0 0 0 0 rgba(68,224,165,.62)}72%,100%{box-shadow:0 0 0 9px rgba(68,224,165,0)}}
2546@keyframes dcPrebidButtonBreathe{0%,72%,100%{transform:translateY(0);box-shadow:0 9px 20px rgba(0,0,0,.15)}82%{transform:translateY(-2px);box-shadow:0 13px 28px rgba(0,0,0,.24),0 0 0 4px rgba(255,255,255,.12)}}
2547@keyframes dcPrebidMobileFloat{0%,68%,100%{transform:translateY(0);box-shadow:0 17px 38px rgba(0,57,44,.3),0 0 0 5px rgba(18,185,129,.11)}80%{transform:translateY(-4px);box-shadow:0 22px 44px rgba(0,57,44,.38),0 0 0 8px rgba(18,185,129,.08)}}
2548@media(prefers-reduced-motion:reduce){.dc-prebid-side-card,.dc-prebid-side-card::after,.dc-prebid-side-copy small::before,.dc-prebid-side-button{animation:none!important;transition:none!important}}
2549.cb-prebid-admin-access{display:flex;justify-content:flex-end;margin:-4px 0 18px}.cb-prebid-admin-access button{display:inline-flex;align-items:center;gap:8px;min-height:43px;border:1px solid #b9d8f2;border-radius:12px;padding:9px 14px;background:linear-gradient(135deg,#002e5f,#0a5ca8);color:#fff;font:900 12px 'Archivo',sans-serif;box-shadow:0 11px 25px rgba(0,46,95,.18);cursor:pointer}.cb-prebid-admin-access button img{width:17px;height:17px;filter:brightness(0) invert(1)}.cb-prebid-admin-access button i{margin-left:3px}.cb-prebid-admin-live{display:inline-block;width:8px;height:8px;flex:0 0 8px;border-radius:50%;background:#36d69b;box-shadow:0 0 0 0 rgba(54,214,155,.55);animation:cbPrebidAdminPulse 1.7s ease-out infinite}
2550.cb-prebid-admin-panel{margin:0 0 25px;overflow:hidden;border:1px solid #cbdfee;border-radius:21px;background:#fff;box-shadow:0 18px 44px rgba(0,46,95,.11);animation:cbPrebidAdminIn .3s ease both}.cb-prebid-admin-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:21px 22px;background:linear-gradient(125deg,#002e5f,#0a5ca8 62%,#087d79);color:#fff}.cb-prebid-admin-head small{color:#90e7d1;font-size:9px;font-weight:950;letter-spacing:.14em}.cb-prebid-admin-head h2{margin:5px 0 3px;font-size:24px}.cb-prebid-admin-head p{margin:0;color:rgba(255,255,255,.78);font-size:12px}.cb-prebid-admin-head-actions{display:flex;align-items:center;justify-content:flex-end;
2550gap:8px;flex-wrap:wrap}.cb-prebid-admin-head-actions>span{display:inline-flex;align-items:center;gap:7px;color:rgba(255,255,255,.82);font-size:11px;font-weight:800}.cb-prebid-admin-head-actions button{width:38px;height:38px;display:grid;place-items:center;border:1px solid rgba(255,255,255,.24);border-radius:11px;background:rgba(255,255,255,.11);cursor:pointer}.cb-prebid-admin-head-actions button img{width:16px;height:16px;filter:brightness(0) invert(1)}
2551.cb-prebid-admin-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding:16px 18px 0}.cb-prebid-admin-metrics article{min-width:0;padding:13px 14px;border:1px solid #e0eaf3;border-radius:13px;background:#f5f9fd}.cb-prebid-admin-metrics small{display:block;color:#75879a;font-size:9px;font-weight:900;letter-spacing:.07em;text-transform:uppercase}.cb-prebid-admin-metrics strong{display:block;margin-top:5px;color:#082e57;font-size:19px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2552.cb-prebid-admin-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px}.cb-prebid-admin-toolbar label{flex:1 1 360px;min-width:0;display:flex;align-items:center;gap:9px;border:1px solid #d7e3ed;border-radius:12px;background:#fff;padding:0 12px}.cb-prebid-admin-toolbar label img{width:16px;height:16px;opacity:.58}.cb-prebid-admin-toolbar input{width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;font:13px 'Archivo',sans-serif}.cb-prebid-admin-toolbar>div{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.cb-prebid-admin-filter{border:1px solid #d4e1ec;border-radius:999px;padding:8px 11px;background:#f7fafc;color:#486078;font:800 11px 'Archivo',sans-serif;cursor:pointer}.cb-prebid-admin-filter.is-active{border-color:#0a5ca8;background:#0a5ca8;color:#fff}
2553.cb-prebid-admin-list{display:grid;gap:10px;padding:0 18px 19px}.cb-prebid-admin-row{display:grid;grid-template-columns:minmax(150px,.6fr) minmax(210px,1fr) minmax(250px,1.35fr);gap:16px;align-items:start;padding:16px;border:1px solid #dce7f0;border-radius:15px;background:#fbfdff}.cb-prebid-admin-row small{display:block;color:#8090a2;font-size:9px;font-weight:900;letter-spacing:.08em}.cb-prebid-admin-offer{display:grid;justify-items:start;gap:5px}.cb-prebid-admin-offer>strong{color:#002e5f;font-size:24px}.cb-prebid-admin-offer>small{font-size:10px;letter-spacing:0}.cb-prebid-admin-status{display:inline-flex;border-radius:999px;padding:5px 8px;font-size:9px;font-weight:900}.cb-prebid-admin-status.is-current{background:#dff7ec;color:#08784f}.cb-prebid-admin-status.is-pending{background:#fff1cc;color:#906100}.cb-prebid-admin-status.is-outbid{background:#eaf0f6;color:#4b6278}.cb-prebid-admin-buyer,.cb-prebid-admin-lot{display:grid;gap:4px;min-width:0}.cb-prebid-admin-buyer strong,.cb-prebid-admin-lot strong{color:#10243a;font-size:14px;overflow-wrap:anywhere}.cb-prebid-admin-buyer a,.cb-prebid-admin-buyer span,.cb-prebid-admin-lot>span{color:#587087;font-size:11px;overflow-wrap:anywhere}.cb-prebid-admin-buyer em{width:max-content;max-width:100%;margin-top:3px;border-radius:8px;padding:5px 7px;background:#f2eafd;color:#6941a5;font-size:10px;font-style:normal;font-weight:800}.cb-prebid-admin-animals{display:grid;gap:5px;margin-top:6px}.cb-prebid-admin-animals>span{display:flex;align-items:flex-start;gap:7px;padding:7px 8px;border-radius:9px;background:#edf5fb;color:#496178;font-size:10px;line-height:1.35}.cb-prebid-admin-animals b{flex:0 0 auto;color:#075ea9}.cb-prebid-admin-message{margin:0 18px 18px;padding:24px;border:1px dashed #c9d9e7;border-radius:14px;background:#f8fbfd;color:#64788c;text-align:center;font-size:12px}.cb-prebid-admin-message.is-error{border-color:#efc5ca;background:#fff5f6;color:#ae2634}
2554.cb-prebid-page{min-height:100vh;background:#f3f7fb;padding:28px clamp(14px,4vw,36px) 110px}.cb-prebid-page-shell{width:min(1180px,100%);margin:0 auto}.cb-prebid-page-back{display:inline-flex;align-items:center;gap:7px;border:0;background:transparent;color:#075ea9;padding:0;font:850 13px 'Archivo',sans-serif;cursor:pointer}.cb-prebid-page-title{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin:18px 0 20px}.cb-prebid-page-title small{color:#087d79;font-size:10px;font-weight:950;letter-spacing:.15em}.cb-prebid-page-title h1{margin:5px 0 4px;color:#10243a;font-size:clamp(28px,4vw,42px);line-height:1.05}.cb-prebid-page-title p{margin:0;color:#64788c;font-size:13px}.cb-prebid-page-live{display:inline-flex;align-items:center;gap:8px;flex:0 0 auto;border:1px solid #c9e9df;border-radius:999px;background:#e9f8f3;color:#08784f;padding:9px 12px;font-size:11px;font-weight:850}.cb-prebid-admin-panel.is-page{margin-bottom:0}
2555.cb-prebid-lot-picker{margin:0 18px 16px;padding:16px;border:1px solid #d8e5ef;border-radius:17px;background:linear-gradient(145deg,#f8fbfe,#eef5fa)}.cb-prebid-lot-picker-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:12px}.cb-prebid-lot-picker-head small{display:block;color:#087d79;font-size:9px;font-weight:950;letter-spacing:.14em}.cb-prebid-lot-picker-head strong{display:block;margin-top:3px;color:#17314a;font-size:16px}.cb-prebid-lot-picker-head span{color:#6a7f92;font-size:11px}.cb-prebid-lot-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:9px;max-height:224px;overflow:auto;padding:1px 3px 3px 1px}.cb-prebid-lot-option{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;border:1px solid #d4e1eb;border-radius:13px;padding:11px 12px;background:#fff;color:#29445d;text-align:left;cursor:pointer;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease,background .18s ease}.cb-prebid-lot-option:hover{transform:translateY(-1px);border-color:#83b8de;box-shadow:0 8px 18px rgba(0,46,95,.08)}.cb-prebid-lot-option.is-active{border-color:#087d79;background:linear-gradient(135deg,#e9f8f3,#f5fbfa);box-shadow:0 0 0 2px rgba(8,125,121,.12),0 9px 22px rgba(8,125,121,.1)}.cb-prebid-lot-option-main{min-width:0}.cb-prebid-lot-option-main strong{display:block;overflow:hidden;color:#12304d;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.cb-prebid-lot-option-main small{display:block;overflow:hidden;margin-top:4px;color:#718498;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.cb-prebid-lot-option-side{text-align:right}.cb-prebid-lot-option-side b{display:block;color:#006b55;font-size:12px}
2555.cb-prebid-lot-option-side span{display:block;margin-top:3px;color:#718498;font-size:9px;font-weight:800}.cb-prebid-selected-context{display:flex;align-items:center;gap:8px;margin:0 18px 2px;color:#526b82;font-size:11px;font-weight:750}.cb-prebid-selected-context strong{color:#12304d}
2556@keyframes cbPrebidAdminPulse{70%,100%{box-shadow:0 0 0 8px rgba(54,214,155,0)}}@keyframes cbPrebidAdminIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
2557@media(max-width:820px){.cb-prebid-admin-head{display:grid}.cb-prebid-admin-head-actions{justify-content:flex-start}.cb-prebid-admin-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.cb-prebid-admin-toolbar{align-items:stretch;flex-direction:column}.cb-prebid-admin-toolbar label{flex-basis:auto}.cb-prebid-admin-row{grid-template-columns:1fr 1fr}.cb-prebid-admin-lot{grid-column:1/-1}}
2558@media(max-width:620px){.cb-prebid-page{padding-top:20px}.cb-prebid-page-title{display:grid;align-items:start}.cb-prebid-page-live{width:max-content}.cb-prebid-page-title h1{font-size:28px}.cb-prebid-lot-picker{margin:0 12px 13px;padding:13px}.cb-prebid-lot-picker-head{display:grid;gap:4px}.cb-prebid-lot-picker-grid{grid-template-columns:1fr;max-height:245px}.cb-prebid-selected-context{margin-inline:12px}}
2559@media(max-width:540px){.cb-prebid-admin-access{justify-content:stretch}.cb-prebid-admin-access button{width:100%;justify-content:center}.cb-prebid-admin-panel{border-radius:17px}.cb-prebid-admin-head{padding:18px}.cb-prebid-admin-metrics{padding:12px 12px 0;gap:7px}.cb-prebid-admin-metrics article{padding:11px}.cb-prebid-admin-metrics strong{font-size:16px}.cb-prebid-admin-toolbar{padding:12px}.cb-prebid-admin-toolbar>div{display:grid;grid-template-columns:repeat(2,1fr)}.cb-prebid-admin-filter{width:100%}.cb-prebid-admin-list{padding:0 12px 14px}.cb-prebid-admin-row{grid-template-columns:1fr;gap:13px;padding:14px}.cb-prebid-admin-lot{grid-column:auto}}
2560@media(prefers-reduced-motion:reduce){.cb-prebid-admin-live,.cb-prebid-admin-panel{animation:none!important}}
2561.dc-lot-contact-head{display:flex;align-items:center;gap:12px;min-width:0}
2562.dc-consignataria-logo-box{display:grid;place-items:center;box-sizing:border-box;padding:3px;line-height:0}
2563.dc-consignataria-logo-box img{display:block;width:auto!important;height:auto!important;max-width:100%!important;max-height:100%!important;object-fit:contain;object-position:center}
2564.rg-detail-list{display:grid;gap:0}
2565.rg-detail-row.dc-rosgan-field-card{grid-template-columns:42px minmax(0,1fr);min-height:52px;padding:8px 0;background:transparent;border:0;border-bottom:1px solid #e9eef4;border-radius:0;box-shadow:none}
2566.rg-detail-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex:none;background:transparent;border:0;box-shadow:none;color:#446783}
2567.rg-detail-icon i{display:block;width:34px;height:34px;background-image:url('assets/remates/rosgan-field-icons-v2.png');background-repeat:no-repeat;background-size:800% 500%;background-color:transparent}
2568.rg-detail-row:last-child{border-bottom:0}
2569.rg-detail-row .dc-rosgan-field-copy{display:grid;grid-template-columns:minmax(120px,34%) minmax(0,1fr);align-items:center;gap:12px}
2570.rg-detail-row .dc-rosgan-field-copy strong{font-size:14px;overflow-wrap:normal;word-break:normal}
2571@media(max-width:480px){.rg-detail-row .dc-rosgan-field-copy{grid-template-columns:minmax(90px,38%) minmax(0,1fr);gap:8px}}
2572.dc-rosgan-field-card{display:grid;grid-template-columns:42px minmax(0,1fr);align-items:center;gap:10px;min-width:0;min-height:68px;background:#f5f8fb;border:1px solid #edf2f7;border-radius:12px;padding:9px 11px 9px 9px;box-sizing:border-box}
2573.dc-rosgan-field-icon-shell{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:#fff;border:1px solid #dce7f1;box-shadow:0 4px 12px rgba(0,46,95,.06);overflow:hidden}
2574.dc-rosgan-field-icon{display:block;width:32px;height:32px;background-image:url('assets/remates/rosgan-field-icons-v1.png');background-repeat:no-repeat;background-size:800% 500%;background-color:transparent}
2575.dc-rosgan-field-copy{display:block;min-width:0}
2576.dc-rosgan-field-copy small{display:block;color:#7e90a6;font-size:11px;font-weight:800;line-height:1.15}
2577.dc-rosgan-field-copy strong{display:block;margin-top:2px;font-size:14px;line-height:1.18;color:#112033;overflow-wrap:anywhere}
2578.dc-rosgan-observations-head{display:flex;align-items:center;gap:9px;color:#8593a6;font-weight:900;letter-spacing:.11em}
2579.dc-rosgan-observations-head img{display:block;width:30px;height:30px;object-fit:contain;flex:none}
2580.dc-lot-contact-avatar{width:52px;height:52px;flex:0 0 52px;display:grid;place-items:center;border-radius:50%;padding:9px;box-sizing:border-box;box-shadow:0 7px 18px rgba(15,42,70,.16);line-height:0}
2581.dc-lot-contact-avatar img{display:block;width:auto!important;height:auto!important;max-width:28px!important;max-height:28px!important;object-fit:contain;object-position:center}
2582.dc-lot-contact-avatar.dc-representative-photo{padding:0;overflow:hidden}
2583.dc-lot-contact-avatar.dc-representative-photo img{width:52px!important;height:52px!important;max-width:none!important;max-height:none!important;object-fit:cover;border-radius:50%}
2584.rg-lot-navigation{display:flex;gap:8px;flex-wrap:wrap}
2585.rg-lot-navigation button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:9px 14px;border:1px solid #cfe0ef;border-radius:999px;background:#fff;color:#002e5f;font:inherit;font-size:12px;font-weight:750;cursor:pointer;box-shadow:0 3px 10px #002e5f06}
2586.rg-lot-navigation button:hover{background:#edf5fb;border-color:#8eaecb}
2587.rg-lot-navigation button:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px}
2588.rg-lot-navigation img{width:15px;height:15px}
2589.dc-lot-contact-avatar strong{color:#fff;font-size:18px}
2590.dc-lot-contact-copy{display:grid;gap:2px;min-width:0}
2591.dc-lot-contact-copy small{color:#8493a7;font-size:9px;font-weight:900;letter-spacing:.1em}
2592.dc-lot-contact-copy strong{color:#10233b;font-size:16px;line-height:1.2}
2593.dc-lot-contact-copy span{color:#607188;font-size:12px;font-weight:600}
2594.dc-lot-contact-actions{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(92px,.65fr);gap:8px;margin-top:14px}
2595.dc-lot-contact-actions a{min-height:42px;display:flex;align-items:center;justify-content:center;gap:8px;border-radius:10px;padding:9px 11px;box-sizing:border-box;font:800 11px 'Archivo',sans-serif;text-decoration:none;cursor:pointer;transition:background-color .18s ease,transform .18s ease,box-shadow .18s ease}
2596.dc-lot-contact-actions a:hover{transform:translateY(-1px)}
2597.dc-lot-contact-actions img{width:16px;height:16px;flex:0 0 16px}
2598.dc-contact-whatsapp{border:1px solid #168c5a;background:#168c5a;color:#fff!important;box-shadow:0 8px 18px rgba(22,140,90,.2)}
2599.dc-contact-whatsapp:hover{background:#11784c;color:#fff!important;box-shadow:0 10px 22px rgba(22,140,90,.26)}
2600.dc-contact-call{border:1px solid #d5e2ef;background:#edf4fb;color:#003b73!important}
2601.dc-contact-call:hover{background:#e2edf8;color:#002e5f!important}
2602.dc-lot-social-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
2603.dc-lot-social-button{min-height:40px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid #d2e2f1;border-radius:999px;background:#fff;color:#0b315d;padding:8px 13px;font:900 12px 'Archivo',sans-serif;box-shadow:0 8px 22px rgba(15,42,70,.08);cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease,background-color .18s ease}
2604.dc-lot-social-button:hover{transform:translateY(-1px);box-shadow:0 12px 28px rgba(15,42,70,.12)}
2605.dc-lot-social-button img{width:16px;height:16px;flex:0 0 16px}
2606.dc-lot-social-button strong{min-width:18px;text-align:center;font-size:13px}
2607.dc-lot-social-button--instagram{border-color:rgba(255,255,255,.28);background:linear-gradient(135deg,#833ab4 0%,#fd1d1d 48%,#fcb045 100%);color:#fff;box-shadow:0 10px 24px rgba(193,53,132,.24)}
2608.dc-lot-social-button--instagram:hover{box-shadow:0 14px 30px rgba(193,53,132,.32)}
2609.dc-lot-social-button--whatsapp{border-color:#168c5a;background:#168c5a;color:#fff;box-shadow:0 10px 24px rgba(22,140,90,.22)}
2610.dc-lot-social-button--whatsapp:hover{background:#11784c;box-shadow:0 14px 30px rgba(22,140,90,.3)}
2611.dc-catalog-export-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
2612.dc-catalog-export-button{min-height:38px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:11px;padding:9px 12px;color:#fff;font:900 12px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 10px 24px rgba(15,42,70,.13);transition:transform .18s ease,box-shadow .18s ease,filter .18s ease}
2613.dc-catalog-export-button:hover{transform:translateY(-1px);box-shadow:0 14px 30px rgba(15,42,70,.18);filter:saturate(1.05)}
2614.dc-catalog-export-button img{width:16px;height:16px;flex:0 0 16px;filter:brightness(0) invert(1)}
2615.dc-catalog-export-button.is-excel{background:#107c41}
2616@media(max-width:680px){.dc-catalog-export-actions{width:100%;display:grid;grid-template-columns:1fr}.dc-catalog-export-button{width:100%;padding:10px 9px;font-size:11px}}
2617.dc-lucide{display:inline-block;width:1em;height:1em;flex:0 0 auto;background:currentColor;vertical-align:-.14em;-webkit-mask:var(--dc-icon) center/contain no-repeat;mask:var(--dc-icon) center/contain no-repeat}
2618.dc-lucide-play{--dc-icon:url('/assets/icons/lucide/play.svg')}.dc-lucide-x{--dc-icon:url('/assets/icons/lucide/x.svg')}.dc-lucide-check{--dc-icon:url('/assets/icons/lucide/check.svg')}
2619.dc-lucide-search{--dc-icon:url('/assets/icons/lucide/search.svg')}.dc-lucide-plus{--dc-icon:url('/assets/icons/lucide/plus.svg')}.dc-lucide-minus{--dc-icon:url('/assets/icons/lucide/minus.svg')}
2620.dc-lucide-arrow-right{--dc-icon:url('/assets/icons/lucide/arrow-right.svg')}.dc-lucide-arrow-left{--dc-icon:url('/assets/icons/lucide/arrow-left.svg')}.dc-lucide-chevron-down{--dc-icon:url('/assets/icons/lucide/chevron-down.svg')}
2621.dc-lucide-map-pin{--dc-icon:url('/assets/icons/lucide/map-pin.svg')}.dc-lucide-calendar{--dc-icon:url('/assets/icons/lucide/calendar-days.svg')}.dc-lucide-radio-tower{--dc-icon:url('/assets/icons/lucide/radio-tower.svg')}
2622.dc-lucide-rotate{--dc-icon:url('/assets/icons/lucide/rotate-ccw.svg')}.dc-lucide-refresh-cw{--dc-icon:url('/assets/icons/lucide/refresh-cw.svg')}.dc-lucide-grid{--dc-icon:url('/assets/icons/lucide/grid-2x2.svg')}.dc-lucide-list{--dc-icon:url('/assets/icons/lucide/list.svg')}
2623.dc-lucide-trending-up{--dc-icon:url('/assets/icons/lucide/trending-up.svg')}.dc-lucide-trending-down{--dc-icon:url('/assets/icons/lucide/trending-down.svg')}.dc-lucide-info{--dc-icon:url('/assets/icons/lucide/info.svg')}.dc-lucide-warning{--dc-icon:url('/assets/icons/lucide/triangle-alert.svg')}
2624.dc-lucide-circle-x{--dc-icon:url('/assets/icons/lucide/circle-x.svg')}.dc-lucide-bold{--dc-icon:url('/assets/icons/lucide/bold.svg')}.dc-lucide-italic{--dc-icon:url('/assets/icons/lucide/italic.svg')}.dc-lucide-underline{--dc-icon:url('/assets/icons/lucide/underline.svg')}
2625.dc-lucide-chevron-up{--dc-icon:url('/assets/icons/lucide/chevron-up.svg')}.dc-lucide-undo{--dc-icon:url('/assets/icons/lucide/undo-2.svg')}.dc-lucide-redo{--dc-icon:url('/assets/icons/lucide/redo-2.svg')}
2626.dc-lucide-upload-cloud{--dc-icon:url('/assets/icons/lucide/upload.svg')}.dc-lucide-file-video{--dc-icon:url('/assets/icons/lucide/video.svg')}
2626.dc-lucide-trash-2{--dc-icon:url('/assets/icons/lucide/trash-2.svg')}
2627.dc-lucide-circle-check{--dc-icon:url('/assets/icons/lucide/circle-check.svg')}.dc-lucide-loader-circle{--dc-icon:url('/assets/icons/lucide/loader-circle.svg');animation:spin .8s linear infinite}
2628.dc-lucide-receipt-text{--dc-icon:url('/assets/icons/lucide/clipboard-list.svg')}.dc-lucide-banknote{--dc-icon:url('/assets/icons/lucide/badge-dollar-sign.svg')}.dc-lucide-beef{--dc-icon:url('/assets/icons/lucide/beef.svg')}.dc-lucide-users{--dc-icon:url('/assets/icons/lucide/users-round.svg')}.dc-lucide-handshake{--dc-icon:url('/assets/icons/lucide/briefcase-business.svg')}
2629.dc-lucide-shopping-cart{--dc-icon:url('/assets/icons/lucide/shopping-cart.svg')}.dc-lucide-store{--dc-icon:url('/assets/icons/lucide/store.svg')}
2630.dc-lucide-shield-check{--dc-icon:url('/assets/icons/lucide/shield-check.svg')}.dc-lucide-circle-alert{--dc-icon:url('/assets/icons/lucide/circle-alert.svg')}.dc-lucide-phone,.dc-lucide-phone-off{--dc-icon:url('/assets/icons/lucide/phone.svg')}.dc-lucide-mail,.dc-lucide-mail-x{--dc-icon:url('/assets/icons/lucide/mail.svg')}.dc-lucide-circle-check-big{--dc-icon:url('/assets/icons/lucide/circle-check-big.svg')}.dc-lucide-arrow-up-right{--dc-icon:url('/assets/icons/lucide/arrow-up-right.svg')}.dc-lucide-calendar-check{--dc-icon:url('/assets/icons/lucide/calendar-days.svg')}
2631.dc-contact-whatsapp img,.dc-lot-social-button--instagram img,.dc-lot-social-button--whatsapp img,.price-board-story-export img,.sales-order-action.primary img{filter:brightness(0) invert(1)}
2632.dc-rp-variant-button{position:relative;width:104px;aspect-ratio:16/10;flex:0 0 104px;overflow:hidden;border:2px solid #cad7e5;border-radius:12px;background:#edf2f7;padding:0;cursor:pointer;box-shadow:none;transition:transform .16s ease,border-color .16s ease,box-shadow .16s ease}
2633.dc-rp-variant-button:hover{transform:translateY(-1px)}
2634.dc-rp-variant-button.is-active{border-color:#0a5ca8;box-shadow:0 0 0 3px rgba(10,92,168,.16),0 10px 24px rgba(0,46,95,.16)}
2635.dc-rp-variant-button.is-active:after{content:"";position:absolute;inset:0;border-radius:10px;box-shadow:inset 0 0 0 2px rgba(255,255,255,.78);pointer-events:none}
2636.dc-rp-variant-check{position:absolute;right:6px;top:6px;width:22px;height:22px;display:grid;place-items:center;border-radius:50%;background:#fff;box-shadow:0 7px 18px rgba(0,30,60,.2)}
2637.dc-rp-variant-check img{width:14px;height:14px}
2638.dc-rp-variant-label{position:absolute;left:6px;bottom:6px;max-width:calc(100% - 12px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px 7px;border-radius:999px;background:rgba(0,46,95,.86);color:#fff;font-size:10px;font-weight:900}
2639.dc-rp-variant-button.is-active .dc-rp-variant-label{background:#0a5ca8}
2640.dc-cabana-video-frame{position:relative;width:100%;max-width:100%;aspect-ratio:16/9;border-radius:18px;overflow:hidden;background:#001d3d;display:grid;place-items:center;color:#c8d8ea;box-shadow:0 18px 48px rgba(0,30,60,.18)}
2641.dc-cabana-empty-media{display:grid;justify-items:center;gap:12px;padding:24px;text-align:center}.dc-cabana-empty-media>span{font-size:13px}.dc-cabana-empty-upload{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;border:1px solid rgba(255,255,255,.28);border-radius:11px;padding:10px 15px;background:#fff;color:#003b75;font:900 12px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 10px 24px rgba(0,0,0,.18);transition:transform .16s,box-shadow .16s}.dc-cabana-empty-upload:hover{transform:translateY(-1px);box-shadow:0 14px 30px rgba(0,0,0,.24)}.dc-cabana-empty-upload img{width:17px;height:17px}.dc-cabana-empty-upload.is-busy{opacity:.72;cursor:wait;pointer-events:none}@media(max-width:600px){.dc-cabana-empty-media{width:100%;box-sizing:border-box;padding:18px}.dc-cabana-empty-upload{width:min(100%,280px)}}
2642.dc-cabana-upload-progress{display:grid;grid-template-columns:42px minmax(150px,230px);align-items:center;gap:12px;padding:12px 14px;border:1px solid rgba(255,255,255,.22);border-radius:14px;background:rgba(0,24,49,.62);box-shadow:0 12px 30px rgba(0,0,0,.22);backdrop-filter:blur(8px);text-align:left}.dc-cabana-upload-ring{width:42px;height:42px;border-radius:50%;display:grid;place-items:center;background:conic-gradient(#33d69f var(--upload-progress),rgba(255,255,255,.16) 0);position:relative}.dc-cabana-upload-ring:before{content:'';position:absolute;inset:5px;border-radius:50%;background:#062b50}.dc-cabana-upload-ring strong{position:relative;z-index:1;color:#fff;font:900 10px 'Archivo',sans-serif}.dc-cabana-upload-copy{display:grid;gap:6px}.dc-cabana-upload-copy strong{color:#fff;font:900 12px 'Archivo',sans-serif}.dc-cabana-upload-copy small{color:#bcd0e4;font-size:10px}.dc-cabana-upload-track{height:6px;overflow:hidden;border-radius:99px;background:rgba(255,255,255,.16)}
2642.dc-cabana-upload-track span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#20bd88,#55e5b5);transition:width .18s ease}@media(max-width:600px){.dc-cabana-upload-progress{grid-template-columns:38px minmax(0,1fr);width:min(100%,290px);box-sizing:border-box}.dc-cabana-upload-ring{width:38px;height:38px}}
2643.dc-cabana-card-upload{position:absolute;z-index:4;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-flex;align-items:center;gap:7px;min-height:36px;padding:8px 11px;border:1px solid rgba(255,255,255,.5);border-radius:10px;background:rgba(255,255,255,.95);color:#003b75;font:900 10px 'Archivo',sans-serif;box-shadow:0 10px 24px rgba(0,0,0,.22);cursor:pointer;white-space:nowrap}.dc-cabana-card-upload img{width:15px;height:15px}.dc-cabana-card-upload:hover{background:#fff;transform:translate(-50%,calc(-50% - 1px))}.dc-cabana-card-upload-progress{position:absolute;z-index:4;left:12px;right:12px;top:50%;transform:translateY(-50%);display:grid;gap:7px;padding:10px;border:1px solid rgba(255,255,255,.24);border-radius:11px;background:rgba(0,24,49,.78);color:#fff;box-shadow:0 10px 26px rgba(0,0,0,.24);backdrop-filter:blur(8px)}.dc-cabana-card-upload-progress>span{display:flex;align-items:center;justify-content:space-between;gap:8px;font:850 10px 'Archivo',sans-serif}.dc-cabana-card-upload-progress small{color:#c8d8ea;font-size:9px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dc-cabana-card-upload-progress .dc-cabana-upload-track{height:7px}
2644@media(max-width:760px){
2645.rd-responsive-toolbar{position:static!important;display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px!important;backdrop-filter:none!important}
2646.rd-responsive-toolbar .rd-search-field{grid-column:1/-1;min-width:0!important;box-sizing:border-box}
2647.rd-responsive-toolbar button{min-width:0!important;max-width:100%;margin:0!important;justify-content:center;min-height:42px;box-sizing:border-box}
2648.rd-responsive-toolbar .rosgan-ops-toolbar{grid-column:1/-1;display:flex;flex-wrap:wrap;width:100%;gap:8px}
2649.rd-responsive-toolbar .rosgan-ops-toolbar button{flex:1 1 110px}
2650.rd-responsive-toolbar .rd-view-switch{grid-column:1/-1;justify-self:end}
2651.rd-responsive-lot-row{display:grid!important;grid-template-columns:64px minmax(0,1fr) minmax(0,1fr) 36px;gap:12px 8px!important;padding:16px 12px!important;align-items:start!important}
2652.rd-row-thumb{grid-column:1;grid-row:1}
2653.rd-row-copy{grid-column:2/4;grid-row:1;overflow-wrap:anywhere}
2654.rd-row-copy>div:last-child{margin-top:5px;line-height:1.4}
2655.rd-row-brand{grid-column:4;grid-row:1}
2656.rd-row-number{grid-column:1;grid-row:2}
2657.rd-row-heads{grid-column:2;grid-row:2;min-width:0!important;text-align:left!important}
2658.rd-row-weight{grid-column:3/5;grid-row:2;min-width:0!important;text-align:left!important}
2659.rd-row-alert{grid-column:1/-1;grid-row:3;width:100%;min-height:40px!important;padding:8px 12px!important}
2660}
2661.rg-video-certifications{position:absolute;top:12px;left:12px;z-index:2;display:flex;gap:8px;pointer-events:none}
2662.rg-video-certification{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;width:64px;height:54px;box-sizing:border-box;border-radius:12px;border:1px solid rgba(255,255,255,.4);box-shadow:0 3px 12px rgba(0,0,0,.2);font-size:10px;font-weight:800;line-height:1}
2663.rg-video-certification img{display:block;width:28px;height:26px;object-fit:contain}
2664.rg-video-visec{background:#f7faef;color:#2b404c;border-color:#90b436}
2665.rg-video-china{background:#fdeef0;color:#9d2235;border-color:#f6ccd2}
2666@media(max-width:640px){.rg-video-certifications{top:9px;left:9px;gap:6px}.rg-video-certification{width:54px;height:42px;border-radius:10px;font-size:9px;gap:2px}.rg-video-certification img{width:23px;height:21px}}
2667.rg-video-cons-brand{position:absolute;top:12px;right:12px;z-index:2;display:grid;place-items:center;width:54px;height:54px;padding:10px;box-sizing:border-box;border-radius:12px;border:1px solid rgba(255,255,255,.4);box-shadow:0 3px 12px rgba(0,0,0,.2);pointer-events:none}
2668.rg-video-cons-brand img{display:block;width:100%;height:100%;object-fit:contain}
2669@media(max-width:640px){.rg-video-cons-brand{top:9px;right:9px;width:42px;height:42px;padding:8px;border-radius:10px}}
2670.dc-cabana-video-frame:fullscreen{width:100vw;height:100vh;aspect-ratio:auto;border-radius:0;background:#000;box-shadow:none}
2671.dc-cabana-video-frame:fullscreen video{width:100vw!important;height:100vh!important;object-fit:contain!important}
2672.dc-video-controls{display:grid;grid-template-columns:auto minmax(120px,1fr) auto auto minmax(72px,110px) auto;align-items:center;gap:9px;padding:9px 10px;border:1px solid #d7e4f0;border-radius:14px;background:#fff;box-shadow:0 10px 26px rgba(0,30,60,.07)}
2673.dc-video-control-button{width:38px;height:38px;display:grid;place-items:center;border:1px solid #d3e1ef;border-radius:12px;background:#f4f8fc;cursor:pointer;transition:transform .16s ease,background-color .16s ease,border-color .16s ease}
2674.dc-video-control-button:hover{transform:translateY(-1px);background:#eaf2fa;border-color:#bed1e5}
2675.dc-video-control-button img{width:18px;height:18px}
2676.dc-video-slider{width:100%;accent-color:#0a6ebd}
2677.dc-video-time{min-width:58px;color:#53677f;font-size:12px;font-weight:900;text-align:center}
2678@media(max-width:760px){.dc-video-controls{grid-template-columns:auto minmax(86px,1fr) auto auto auto;gap:7px;padding:8px}.dc-video-time{min-width:48px;font-size:11px}.dc-video-volume{display:none}.dc-rp-variant-button{width:96px;flex-basis:96px}}
2679.dc-catalog-contact-row{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;margin:-8px 0 22px}
2680.dc-catalog-contact-row .dc-lot-contact-card{box-shadow:0 10px 28px rgba(15,42,70,.08)}
2681.dc-catalog-floating-contact{position:fixed;left:12px;right:12px;bottom:12px;z-index:88;display:none;align-items:center;gap:10px;padding:10px;border:1px solid #dbe7f2;border-radius:16px;background:rgba(255,255,255,.96);box-shadow:0 16px 38px rgba(0,30,60,.2);backdrop-filter:blur(12px);opacity:0;pointer-events:none;transform:translateY(18px) scale(.98);transition:opacity .22s ease,transform .22s ease}
2682.dc-catalog-floating-contact.is-visible{opacity:1;pointer-events:auto;transform:translateY(0) scale(1)}
2683.dc-catalog-floating-avatar{width:40px;height:40px;flex:0 0 40px;display:grid;place-items:center;border-radius:50%;padding:7px;box-sizing:border-box;box-shadow:0 8px 20px rgba(15,42,70,.18)}
2684.dc-catalog-floating-avatar img{display:block;width:100%;height:100%;object-fit:contain}
2685.dc-catalog-floating-avatar strong{color:#fff;font-size:15px}
2686.dc-catalog-floating-copy{min-width:0;display:grid;gap:1px;flex:1}
2687.dc-catalog-floating-copy small{color:#8493a7;font-size:9px;font-weight:900;letter-spacing:.1em;text-transform:uppercase}
2688.dc-catalog-floating-copy strong{overflow:hidden;color:#102033;font-size:13px;text-overflow:ellipsis;white-space:nowrap}
2689.dc-catalog-floating-contact a{width:40px;height:40px;flex:0 0 40px;display:grid;place-items:center;border-radius:12px;text-decoration:none}
2690.dc-catalog-floating-contact img{width:18px;height:18px}
2691@media(max-width:760px){html[data-public-advance="lacassina"][data-public-route="lacassina"] .dc-catalog-floating-contact{display:flex}html[data-public-advance="lacassina"][data-public-route="lacassina"] main{padding-bottom:86px}}
2692.cb-auditor{margin:0 0 24px;padding:20px;border:1px solid #dce5ef;border-radius:17px;background:#fff}.cb-auditor header{display:flex;justify-content:space-between;gap:14px;align-items:center;flex-wrap:wrap}.cb-auditor h2{margin:0;color:#112f4e}.cb-auditor p,.cb-auditor small{color:#65758a}.cb-auditor-search{width:100%;box-sizing:border-box;margin:16px 0;padding:12px;border:1px solid #cbd8e7;border-radius:10px;font:inherit}.cb-auditor-row{display:grid;grid-template-columns:minmax(170px,1fr) minmax(280px,1.3fr) minmax(210px,1fr);gap:20px;padding:18px 0;border-top:1px solid #e7edf4;align-items:center}.cb-auditor-row small{display:block;margin-top:4px}.cb-auditor-pricing{display:flex;align-items:end;gap:8px;flex-wrap:wrap}.cb-auditor-pricing label{flex:1;min-width:90px;font-size:11px;color:#65758a}.cb-auditor-pricing input{box-sizing:border-box;width:100%;margin-top:5px;border:1px solid #cbd8e7;border-radius:8px;padding:10px;font:inherit;color:#12304d}.cb-auditor button,.cb-auditor-upload{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid #cbd8e7;border-radius:9px;padding:10px;background:#f4f8fc;color:#084b89;font:700 12px inherit;cursor:pointer}.cb-auditor button:disabled{opacity:.5;cursor:wait}.cb-auditor-actions{display:flex;gap:7px;flex-wrap:wrap}.cb-auditor-actions img{width:15px;height:15px}.cb-auditor-upload{position:relative;overflow:hidden}.cb-auditor-upload input{position:absolute;inset:0;opacity:0;width:100%;cursor:pointer}.cb-auditor-upload:focus-within{outline:2px solid #0a5ca8;outline-offset:2px}@media(max-width:950px){.cb-auditor-row{grid-template-columns:1fr}.cb-auditor{padding:15px}}
2693.cb-auditor-page .cb-auditor-actions{display:flex;align-items:center;gap:6px;flex-wrap:nowrap}.cb-auditor-page .cb-auditor-actions button,.cb-auditor-page .cb-auditor-upload{box-sizing:border-box;flex:0 0 auto;height:36px;min-height:36px;padding:0 10px;border:1px solid #d5e2ef;border-radius:8px;background:#fff;color:#16466f;font-family:inherit;font-size:12px;font-weight:600;line-height:1;white-space:nowrap;transition:background .15s,border-color .15s}.cb-auditor-page .cb-auditor-actions .cb-auditor-edit{width:36px;padding:0}.cb-auditor-page .cb-auditor-upload{background:#edf5fd;border-color:#cbdff2;color:#075ba4}.cb-auditor-page .cb-auditor-actions button:hover,.cb-auditor-page .cb-auditor-upload:hover{background:#e2eef9;border-color:#9ebddd}.cb-auditor-page .cb-auditor-actions img{display:block;width:15px;height:15px;flex:none}
2693.cb-auditor-page .cb-auditor-upload:has(input:disabled){opacity:.5;cursor:wait}.cb-auditor-page .cb-auditor-actions .rf-camera-entry{display:none}
2694.cb-auditor-page .cb-auditor-row{align-items:start}.cb-auditor-page .cb-auditor-pricing{margin:0;align-items:end}.cb-auditor-page .cb-auditor-pricing label{display:grid;gap:5px;line-height:16px}.cb-auditor-page .cb-auditor-pricing input{height:36px;margin:0;padding:0 10px}.cb-auditor-page .cb-auditor-pricing button{box-sizing:border-box;height:36px;min-height:36px;padding:0 12px;font-family:inherit;font-size:12px;line-height:1}.cb-auditor-page .cb-auditor-action-cell{padding-top:21px}.cb-auditor-page .cb-auditor-action-cell>small:empty{display:none}@media(max-width:950px){.cb-auditor-page .cb-auditor-action-cell{padding-top:0}}
2695.cb-auditor-page.ra-panel{position:relative;inset:auto;width:100%;height:auto;min-height:calc(100dvh - 65px);margin:0;border:0;border-radius:0;padding:0;overflow:visible;background:#f4f7fa}.cb-auditor-page .ra-content{display:flex}.cb-auditor-page .cb-auditor-row{padding:14px 16px;min-height:86px;gap:12px}.cb-auditor-page .cb-auditor-row:hover{background:#f7faff}.cb-auditor-columns{display:grid;grid-template-columns:minmax(170px,1fr) minmax(280px,1.3fr) minmax(210px,1fr);gap:12px;padding:12px 16px;background:#fafbfd;color:#687b8e;font-size:10px;letter-spacing:.65px}.cb-auditor-page .ra-list{min-height:240px}.cb-auditor-page .ra-top{flex-wrap:nowrap}.cb-auditor-page .ra-search input{margin:0}.cb-auditor-page .ra-private{white-space:nowrap}@media(max-width:950px){.cb-auditor-columns{display:none}.cb-auditor-page .ra-top,.cb-auditor-page .ra-content{padding:16px}.cb-auditor-page .ra-private{display:none}}
2696.cb-auction-subpages{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 16px}.cb-auction-subpages button{display:inline-flex;align-items:center;gap:7px;min-height:38px;border:1px solid #cbd8e7;border-radius:10px;padding:8px 11px;background:#fff;color:#12304d;font:800 12px inherit;cursor:pointer;transition:border-color .18s,box-shadow .18s,transform .18s}.cb-auction-subpages button:hover{border-color:#8faccc;box-shadow:0 6px 16px rgba(15,42,70,.08);transform:translateY(-1px)}.cb-auction-subpages img{width:15px;height:15px}.cb-auction-subpages .is-tv{border-color:#b9d9d7;background:#f0fbfa;color:#08756f}.cb-auction-subpages+div button[title="Ver estadísticas del remate"]{display:none!important}@media(max-width:520px){.cb-auction-subpages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.cb-auction-subpages button{justify-content:center}.cb-auction-subpages button:last-child{grid-column:1/-1}}
2697.cb-catalog-access-setting{display:flex;align-items:center;justify-content:space-between;gap:20px;background:#fff;border:1px solid #d4e3ef;border-radius:18px;padding:22px;margin-bottom:16px}.cb-catalog-access-setting h2{font-size:17px;margin:0 0 8px}.cb-catalog-access-setting p{font-size:13px;color:#60758c;line-height:1.5;margin:0 0 10px;max-width:650px}.cb-catalog-access-setting strong{font-size:12px;color:#075da8}.cb-catalog-access-switch{flex:none;width:58px;height:36px;border:0;border-radius:99px;padding:4px;cursor:pointer}.cb-catalog-access-switch span{display:block;width:28px;height:28px;background:#fff;border-radius:50%;transition:transform .18s}.cb-catalog-access-switch:focus-visible{outline:3px solid #72b4ec;outline-offset:3px}.cb-catalog-registration{text-align:center;padding:36px 20px;background:#fff;border:1px solid #d4e3ef;border-radius:18px}.cb-catalog-registration>img{width:30px;height:30px}.cb-catalog-registration p{color:#60758c;line-height:1.5}.cb-catalog-registration button{min-height:44px;padding:12px 20px;border:0;border-radius:10px;background:#075da8;color:#fff;font-weight:700;cursor:pointer}
2698.cb-detail-hero{position:relative;isolation:isolate;overflow:hidden;color:var(--cb-hero-text);background:var(--cb-hero-color)}
2699.cb-detail-hero-shell{position:relative;z-index:3;max-width:1240px;margin:0 auto;padding:20px clamp(18px,4vw,44px) 30px}
2700.cb-detail-back{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--cb-hero-border);border-radius:999px;background:var(--cb-hero-surface);color:var(--cb-hero-text);padding:8px 12px;font:800 12px inherit;cursor:pointer;transition:background .18s,border-color .18s,transform .18s}
2701.cb-detail-back:hover{background:var(--cb-hero-surface-strong);transform:translateX(-2px)}
2702.cb-detail-hero-main{display:grid;grid-template-columns:minmax(0,1fr);gap:clamp(22px,4vw,58px);align-items:center;margin-top:20px}
2703.cb-detail-hero-main:has(.cb-detail-brand-panel){grid-template-columns:minmax(0,1.1fr) minmax(330px,.9fr)}
2704.cb-detail-copy{min-width:0}
2705.cb-detail-breed{display:inline-flex;align-items:center;gap:7px;padding:6px 11px;border:1px solid var(--cb-hero-border);border-radius:999px;background:var(--cb-hero-surface);font-size:10px;font-weight:950;letter-spacing:.11em;text-transform:uppercase}
2706.cb-detail-breed:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--cb-hero-text);box-shadow:0 0 0 4px var(--cb-hero-surface-strong)}
2707.cb-detail-title{max-width:720px;margin:12px 0 15px;font-size:clamp(32px,4.2vw,52px);font-weight:950;letter-spacing:-.035em;line-height:.98;text-wrap:balance}
2708.cb-detail-meta{display:flex;gap:9px;flex-wrap:wrap}
2709.cb-detail-meta-item{min-height:39px;display:inline-flex;align-items:center;gap:9px;padding:8px 11px;border:1px solid var(--cb-hero-border);border-radius:11px;background:var(--cb-hero-surface);color:var(--cb-hero-text);font-size:12px;font-weight:800}
2710.cb-detail-meta-item img{width:16px;height:16px;filter:var(--cb-hero-icon-filter);opacity:.82}
2711.cb-detail-counts{display:flex;gap:9px;flex-wrap:wrap;margin-top:17px}
2712.cb-detail-count{min-width:112px;display:grid;grid-template-columns:auto 1fr;column-gap:9px;align-items:center;padding:10px 12px;border:1px solid var(--cb-hero-border);border-radius:13px;background:var(--cb-hero-surface)}
2713.cb-detail-count strong{grid-row:1/3;font-size:24px;line-height:1;font-weight:950}.cb-detail-count small{align-self:end;color:var(--cb-hero-muted);font-size:9px;font-weight:950;letter-spacing:.1em}.cb-detail-count span{align-self:start;color:var(--cb-hero-text);font-size:10px;font-weight:750;opacity:.86}
2714.cb-detail-counts.is-loading{display:grid;grid-template-columns:repeat(3,minmax(110px,132px))}.cb-detail-counts.is-loading .dc-skeleton{height:62px;border-radius:13px}
2715.cb-detail-brand-panel{min-width:0;display:grid;gap:14px;justify-items:center;align-content:center;padding:16px 8px}
2716.cb-detail-main-logos{min-height:104px;display:flex;align-items:center;justify-content:center;gap:22px;flex-wrap:wrap;width:100%}
2717.cb-detail-main-brand{width:clamp(160px,18vw,220px);height:104px;min-width:0;flex:0 1 220px;display:flex;align-items:center;justify-content:center;gap:11px}.cb-detail-main-brand img{display:block;width:100%;height:100%;object-fit:contain;filter:var(--cb-logo-filter,none)}.cb-detail-main-brand strong{color:var(--cb-hero-text);font-size:24px;text-align:center}
2718.cb-detail-guests{display:flex;align-items:center;justify-content:center;gap:11px;flex-wrap:wrap}.cb-detail-guests>span{color:var(--cb-hero-muted);font-size:9px;font-weight:950;letter-spacing:.12em}.cb-detail-guest-logo{height:34px;min-width:46px;max-width:126px;display:flex;align-items:center;justify-content:center;padding:2px 4px;border-radius:8px;color:var(--cb-hero-text)}.cb-detail-guest-logo img{width:auto;max-width:108px;height:30px;object-fit:contain}.cb-detail-guest-logo small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;font-weight:900}
2719@media(max-width:820px){.cb-detail-hero-shell{padding:16px 16px 22px}.cb-detail-hero-main:has(.cb-detail-brand-panel){grid-template-columns:1fr;gap:16px}.cb-detail-title{font-size:clamp(31px,9vw,44px);margin-bottom:13px}.cb-detail-brand-panel{padding:4px 8px 0}.cb-detail-main-logos{min-height:72px}.cb-detail-main-brand{width:160px;height:72px;flex-basis:160px}}
2720@media(max-width:480px){.cb-detail-back{max-width:100%;padding:7px 10px;font-size:11px}.cb-detail-hero-main{margin-top:17px}.cb-detail-meta{display:grid;grid-template-columns:1fr}.cb-detail-meta-item{width:100%;box-sizing:border-box}.cb-detail-counts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.cb-detail-count{min-width:0;display:block;padding:10px}.cb-detail-count strong{display:block;font-size:21px}.cb-detail-count small{display:block;margin-top:4px}.cb-detail-count span{display:none}.cb-detail-counts.is-loading{grid-template-columns:repeat(3,minmax(0,1fr))}}
2721
2722.cb-detail-brand-heading{display:flex;align-items:center;justify-content:space-between;width:100%;font-size:10px;font-weight:850;letter-spacing:.1em;color:var(--cb-hero-muted)}
2723.cb-detail-brand-heading span{border-radius:20px;padding:4px 8px;background:var(--cb-hero-surface-strong);color:var(--cb-hero-text);letter-spacing:0}
2724.cb-detail-brand-panel.is-compact{padding:8px 0;gap:12px;align-content:start;width:100%}
2725.cb-name-roll{width:100%;min-width:0}
2726.cb-name-roll-window{height:192px;overflow:auto;scrollbar-width:none;overscroll-behavior:contain;mask-image:linear-gradient(transparent,#000 15%,#000 85%,transparent)}
2727.cb-name-roll-window::-webkit-scrollbar{display:none}
2728.cb-name-roll-track{animation:cbNameRoll var(--cb-roll-duration,45s) linear infinite;will-change:transform}
2729.cb-name-roll-group{display:flex;flex-direction:column}
2730.cb-name-roll-item{display:flex;align-items:center;gap:14px;height:64px;flex:none;box-sizing:border-box;padding:8px 2px;border-bottom:1px solid var(--cb-hero-border)}
2731.cb-name-roll-item strong{font-size:clamp(16px,1.55vw,22px);font-weight:750;line-height:1.2;letter-spacing:-.02em;color:var(--cb-hero-text);overflow-wrap:anywhere}
2732.cb-name-roll:hover .cb-name-roll-track,.cb-name-roll:focus-within .cb-name-roll-track,.cb-name-roll.is-paused .cb-name-roll-track{animation-play-state:paused}
2733.cb-name-roll-footer{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:10px;font-size:11px;color:var(--cb-hero-muted)}
2734.cb-name-roll-footer button{min-height:36px;border:1px solid var(--cb-hero-border);border-radius:20px;padding:7px 13px;background:var(--cb-hero-surface);color:var(--cb-hero-text);font:700 11px inherit;cursor:pointer}
2735.cb-name-roll button:focus-visible,.cb-name-roll-window:focus-visible{outline:2px solid var(--cb-hero-text);outline-offset:3px}
2736@keyframes cbNameRoll{to{transform:translateY(-50%)}}
2737@media(max-width:820px){.cb-name-roll-window{height:168px}.cb-name-roll-item{height:56px}.cb-name-roll-item strong{font-size:17px}}
2738@media(prefers-reduced-motion:reduce){.cb-name-roll-track{animation:none;will-change:auto}.cb-name-roll-copy,.cb-name-roll-footer{display:none}.cb-name-roll-window{mask-image:none;scrollbar-width:thin}}
2739.tv-bug-live{min-height:46px;gap:12px;font-size:18px}
2740.tv-bug-clock{min-height:44px;padding:0 18px 0 20px;gap:10px;font-size:21px}
2741.tv-bug-clock:before{display:none}
2742.tv-bug-clock img{width:19px;height:19px;flex:0 0 19px;filter:brightness(0) invert(1);opacity:.92}
2743.tv-bug-live-card{min-height:44px;max-width:660px;padding:0 20px;gap:12px}
2744.tv-bug-live-card b{gap:9px;font-size:17px}
2745.tv-bug-live-card b:before{width:9px;height:9px}
2746.tv-bug-live-card span{font-size:20px}
2747.tv-weather-bug{position:absolute;top:4.2vh;right:3vw;z-index:4;min-height:58px;display:flex;align-items:center;gap:13px;padding:0 20px 0 18px;border:1px solid rgba(255,255,255,.24);border-radius:999px 0 0 999px;background:linear-gradient(90deg,rgba(4,22,39,.82),rgba(10,92,168,.72));color:#fff;box-shadow:0 18px 38px rgba(0,0,0,.22),inset 0 1px rgba(255,255,255,.18);backdrop-filter:blur(12px);filter:drop-shadow(0 16px 28px rgba(0,0,0,.22))}
2748.tv-weather-icon{width:30px;height:30px;flex:0 0 30px;display:block;filter:brightness(0) invert(1) drop-shadow(0 1px 2px rgba(0,0,0,.18));opacity:.96}
2749.tv-weather-temp{font-size:28px;font-weight:950;line-height:1;letter-spacing:.01em}
2750.tv-weather-copy{display:grid;gap:2px;min-width:0;line-height:1}
2751.tv-weather-copy strong{font-size:13px;font-weight:950;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,.94)}
2752.tv-weather-copy small{max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:850;color:rgba(255,255,255,.76)}
2753.tv-waiting-screen{--tv-wait-color:#00529b;position:fixed;inset:0;z-index:30;display:grid;place-items:center;width:100vw;height:100vh;overflow:hidden;background:linear-gradient(125deg,color-mix(in srgb,var(--tv-wait-color) 78%,transparent) 0%,rgba(0,20,48,.8) 34%,color-mix(in srgb,var(--tv-wait-color) 62%,transparent) 64%,rgba(0,72,95,.72) 100%);background-size:320% 320%;color:#fff;font-family:'Archivo',Arial,sans-serif;backdrop-filter:saturate(.7) contrast(.92);animation:tvWaitingGradient 8s cubic-bezier(.45,.05,.55,.95) infinite}
2754.tv-waiting-screen:before{content:"";position:absolute;inset:-30%;background:radial-gradient(circle at 20% 30%,rgba(255,255,255,.14),transparent 25%),radial-gradient(circle at 76% 28%,rgba(39,222,224,.2),transparent 27%),radial-gradient(circle at 52% 78%,rgba(255,255,255,.1),transparent 28%);filter:blur(10px);animation:tvWaitingAurora 10s ease-in-out infinite alternate}
2755.tv-waiting-screen:after{content:"";position:absolute;inset:0;opacity:.16;background-image:linear-gradient(rgba(255,255,255,.12) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.12) 1px,transparent 1px);background-size:72px 72px;mask-image:linear-gradient(to bottom,rgba(0,0,0,.8),transparent 86%);animation:tvWaitingGrid 12s linear infinite}
2756.tv-waiting-orb{position:absolute;z-index:0;width:32vw;aspect-ratio:1;border-radius:50%;border:1px solid rgba(255,255,255,.24);box-shadow:inset 0 0 90px rgba(255,255,255,.08),0 0 120px rgba(16,195,205,.2);backdrop-filter:blur(2px)}
2757.tv-waiting-orb.is-one{left:-12vw;top:-15vw;animation:tvWaitingOrbOne 9s ease-in-out infinite alternate}.tv-waiting-orb.is-two{right:-10vw;bottom:-17vw;width:38vw;animation:tvWaitingOrbTwo 11s ease-in-out infinite alternate}
2758.tv-waiting-frame{position:relative;z-index:2;width:min(92vw,164vh);min-height:min(82vh,820px);
2758display:grid;grid-template-rows:auto 1fr;gap:clamp(34px,4.5vh,62px);padding:clamp(32px,4.2vw,72px);box-sizing:border-box;border:1px solid rgba(255,255,255,.14);border-radius:clamp(28px,3vw,52px);background:linear-gradient(145deg,rgba(2,22,46,.23),rgba(0,28,58,.09));box-shadow:0 42px 120px rgba(0,16,38,.32),inset 0 1px rgba(255,255,255,.15);backdrop-filter:blur(7px)}
2759.tv-waiting-logos{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(38px,6vw,110px);align-items:center;padding:0 clamp(12px,3vw,52px)}
2760.tv-waiting-logo{position:relative;min-height:clamp(96px,12vh,150px);display:flex;align-items:center;justify-content:center;padding:0;box-sizing:border-box;animation:tvWaitingLogoFloat 4.8s ease-in-out infinite}
2761.tv-waiting-logo:nth-child(2){animation-delay:-1.3s}.tv-waiting-logo:nth-child(3){animation-delay:-2.7s}
2762.tv-waiting-logo img{display:block;max-width:100%;max-height:clamp(82px,10vh,126px);object-fit:contain;filter:var(--tv-wait-logo-filter,none) drop-shadow(0 16px 28px rgba(0,0,0,.36))}.tv-waiting-logo.is-rosgan img{max-width:92%;filter:brightness(0) invert(1) drop-shadow(0 16px 28px rgba(0,0,0,.34))}.tv-waiting-logo strong{max-width:100%;font-size:clamp(24px,2vw,38px);font-weight:950;text-align:center;text-transform:uppercase;text-shadow:0 15px 30px rgba(0,0,0,.35)}
2763.tv-waiting-body{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(410px,.95fr);gap:clamp(34px,5vw,84px);align-items:center}
2764.tv-waiting-copy{display:grid;align-content:center;gap:clamp(18px,2.2vh,28px);min-width:0}.tv-waiting-kicker{display:inline-flex;align-items:center;gap:12px;width:max-content;max-width:100%;font-size:clamp(12px,1vw,17px);font-weight:950;letter-spacing:.2em;text-transform:uppercase;color:rgba(255,255,255,.78)}.tv-waiting-kicker:before{content:"";width:38px;height:3px;border-radius:999px;background:#fff;box-shadow:0 0 22px rgba(255,255,255,.7)}
2765.tv-waiting-title{margin:0;max-width:900px;font-size:clamp(38px,4vw,72px);font-weight:950;line-height:.98;letter-spacing:-.045em;text-wrap:balance;text-shadow:0 12px 36px rgba(0,18,42,.28)}
2766.tv-waiting-facts{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,.7fr);gap:12px}.tv-waiting-fact{min-width:0;min-height:64px;display:grid;align-content:center;gap:5px;padding:12px 17px;border:1px solid rgba(255,255,255,.16);border-radius:17px;background:rgba(0,24,52,.2)}.tv-waiting-fact.is-location{grid-column:1/-1}.tv-waiting-fact small{color:rgba(255,255,255,.62);font-size:10px;font-weight:950;letter-spacing:.14em;text-transform:uppercase}.tv-waiting-fact strong{font-size:clamp(13px,1.05vw,18px);font-weight:900;line-height:1.15;text-wrap:balance}.tv-waiting-fact.is-location strong{font-size:clamp(12px,1vw,17px)}
2767.tv-waiting-countdown{position:relative;display:grid;gap:clamp(18px,2.2vh,28px);padding:clamp(28px,3.2vw,50px);border:1px solid rgba(255,255,255,.26);border-radius:32px;background:linear-gradient(150deg,rgba(255,255,255,.16),rgba(0,20,44,.28));box-shadow:0 28px 68px rgba(0,15,36,.28),inset 0 1px rgba(255,255,255,.22);backdrop-filter:blur(15px)}
2768.tv-waiting-countdown:before{content:"";position:absolute;inset:10px;border:1px solid rgba(255,255,255,.08);border-radius:24px;pointer-events:none}.tv-waiting-countdown-label{position:relative;color:rgba(255,255,255,.74);font-size:clamp(11px,.95vw,16px);font-weight:950;letter-spacing:.16em;text-align:center;text-transform:uppercase}
2769.tv-waiting-count-grid{position:relative;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(7px,.75vw,13px)}.tv-waiting-count-part{display:grid;gap:7px;text-align:center}.tv-waiting-count-part strong{display:grid;place-items:center;min-height:clamp(68px,7.6vw,116px);border:1px solid rgba(255,255,255,.22);border-radius:18px;background:rgba(0,20,44,.28);font-size:clamp(34px,3.5vw,62px);font-variant-numeric:tabular-nums;font-weight:950;line-height:1;box-shadow:inset 0 1px rgba(255,255,255,.16)}.tv-waiting-count-part small{color:rgba(255,255,255,.64);font-size:clamp(8px,.72vw,12px);font-weight:950;letter-spacing:.13em;text-transform:uppercase}
2770.tv-waiting-message{position:relative;display:flex;align-items:center;justify-content:center;min-height:clamp(120px,14vh,180px);font-size:clamp(30px,3.1vw,54px);font-weight:950;line-height:1.02;text-align:center;text-wrap:balance;text-shadow:0 10px 30px rgba(0,0,0,.2);animation:tvWaitingMessagePulse 2.2s ease-in-out infinite}
2771.tv-waiting-local-time{position:relative;display:flex;justify-content:center;gap:8px;color:rgba(255,255,255,.62);font-size:clamp(10px,.82vw,14px);font-weight:850}.tv-waiting-local-time strong{color:#fff;font-variant-numeric:tabular-nums}
2772.tv-end-frame{grid-template-rows:auto 1fr;gap:clamp(22px,3.2vh,44px)}
2773.tv-end-logos{gap:clamp(44px,7vw,132px);padding:0 clamp(8px,2vw,36px)}
2774.tv-end-logos .tv-waiting-logo{min-height:clamp(120px,17vh,210px)}
2775.tv-end-logos .tv-waiting-logo img{max-height:clamp(104px,15vh,188px)}
2776.tv-end-copy{position:relative;display:grid;align-content:center;justify-items:center;gap:clamp(15px,2vh,26px);padding-bottom:clamp(18px,3vh,42px);text-align:center}
2777.tv-end-kicker{display:flex;align-items:center;gap:16px;color:rgba(255,255,255,.7);font-size:clamp(12px,1vw,17px);font-weight:950;letter-spacing:.24em;text-transform:uppercase}.tv-end-kicker:before,.tv-end-kicker:after{content:"";width:clamp(38px,5vw,82px);height:2px;border-radius:999px;background:linear-gradient(90deg,transparent,#fff)}
2777.tv-end-kicker:after{transform:scaleX(-1)}
2778.tv-end-title{margin:0;color:#fff;font-size:clamp(64px,8vw,148px);font-weight:950;line-height:.86;letter-spacing:-.055em;text-wrap:balance;text-shadow:0 18px 52px rgba(0,12,30,.36)}
2779.tv-end-thanks{margin:0;color:rgba(255,255,255,.9);font-size:clamp(26px,3vw,54px);font-weight:850;line-height:1.05;letter-spacing:-.025em;text-shadow:0 12px 34px rgba(0,12,30,.3)}
2780.tv-end-copy:after{content:"";width:clamp(90px,12vw,210px);height:5px;margin-top:clamp(4px,1vh,12px);border-radius:999px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.95),transparent);box-shadow:0 0 28px rgba(255,255,255,.32)}
2781@keyframes tvWaitingGradient{0%{background-position:0% 50%}50%{background-position:100% 45%}100%{background-position:0% 50%}}@keyframes tvWaitingAurora{0%{transform:translate3d(-4%,-2%,0) rotate(-4deg) scale(.95)}100%{transform:translate3d(5%,4%,0) rotate(7deg) scale(1.1)}}@keyframes tvWaitingGrid{to{background-position:72px 72px}}@keyframes tvWaitingOrbOne{to{transform:translate(10vw,7vh) scale(1.18)}}@keyframes tvWaitingOrbTwo{to{transform:translate(-8vw,-6vh) scale(.88)}}@keyframes tvWaitingLogoFloat{0%,100%{transform:translateY(0) rotateX(0)}50%{transform:translateY(-8px) rotateX(3deg)}}@keyframes tvWaitingMessagePulse{0%,100%{opacity:.78;transform:scale(.98)}50%{opacity:1;transform:scale(1)}}
2782@media (max-aspect-ratio:4/3){.tv-waiting-frame{width:92vw;min-height:88vh}.tv-waiting-body{grid-template-columns:1fr}.tv-waiting-title{text-align:center}.tv-waiting-kicker{margin:auto}.tv-waiting-copy{gap:16px}.tv-waiting-countdown{padding:24px}.tv-waiting-facts{display:none}.tv-end-frame{min-height:84vh}.tv-end-logos{gap:24px}.tv-end-logos .tv-waiting-logo{min-height:100px}}
2783.tv-waiting-broadcast{background:linear-gradient(90deg,rgba(3,16,29,.98),rgba(3,19,34,.94) 48%,rgba(3,20,33,.78));animation:none;backdrop-filter:saturate(.45)}
2784.tv-waiting-broadcast:before{inset:0;background:radial-gradient(ellipse at 100% 0%,rgba(47,172,188,.19),transparent 60%);filter:none;animation:none}
2785.tv-waiting-broadcast:after{inset:0;background:linear-gradient(115deg,transparent 65%,rgba(255,255,255,.035) 65%,transparent 88%);opacity:1;mask-image:none;animation:none}
2786.tv-waiting-broadcast .tv-waiting-orb{display:none}
2787.tv-waiting-broadcast .tv-waiting-frame{width:90vw;height:86vh;min-height:0;padding:0;grid-template-rows:auto 1fr auto;gap:5vh;border:0;border-radius:0;background:none;box-shadow:none;backdrop-filter:none}
2788.tv-waiting-broadcast .tv-waiting-logos{grid-template-columns:1fr 1fr 1fr;gap:4vw;padding:0 0 3vh;border-bottom:1px solid rgba(255,255,255,.18)}
2789.tv-waiting-broadcast .tv-waiting-logo{min-height:12vh;animation:tvWaitingLogoFloat 4.8s ease-in-out infinite;justify-content:center}
2790.tv-waiting-broadcast .tv-waiting-logo:nth-child(2){animation-delay:-1.3s}.tv-waiting-broadcast .tv-waiting-logo:nth-child(3){animation-delay:-2.7s}
2791.tv-waiting-broadcast .tv-waiting-logo.is-rosgan{justify-content:flex-start}
2792.tv-waiting-broadcast .tv-waiting-logo:last-child{justify-content:flex-end}
2793.tv-waiting-broadcast .tv-waiting-logo img{max-height:12vh;max-width:23vw;filter:var(--tv-wait-logo-filter,none)}
2794.tv-waiting-broadcast .tv-waiting-logo.is-rosgan img{filter:brightness(0) invert(1)}
2795.tv-waiting-broadcast .tv-waiting-logo strong{font-size:clamp(18px,2vw,34px);text-shadow:none}
2796.tv-waiting-broadcast .tv-waiting-body{grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:5vw}
2797.tv-waiting-broadcast .tv-waiting-copy{gap:3vh}
2798.tv-waiting-broadcast .tv-waiting-kicker{font-size:clamp(15px,1.35vw,26px);letter-spacing:.12em;color:#7cd2d7}
2799.tv-waiting-broadcast .tv-waiting-kicker:before{width:24px;height:2px;background:#7cd2d7;box-shadow:none}
2800.tv-waiting-broadcast .tv-waiting-title{font-size:clamp(30px,4.4vw,86px);line-height:1.05;letter-spacing:-.04em;text-shadow:none;text-align:left}
2801.tv-waiting-broadcast .tv-waiting-facts{display:grid;gap:2vh 2vw;margin-top:1vh;grid-template-columns:1.65fr 1fr}
2802.tv-waiting-broadcast .tv-waiting-fact{padding:0;min-height:0;border:0;border-radius:0;background:none;gap:8px;align-content:start}
2803.tv-waiting-broadcast .tv-waiting-fact small{color:#b4cad8;font-size:clamp(14px,1.15vw,22px);letter-spacing:.1em}
2804.tv-waiting-broadcast .tv-waiting-fact strong{font-size:clamp(19px,1.8vw,34px);line-height:1.25;font-weight:700;text-wrap:pretty}
2805.tv-waiting-broadcast .tv-waiting-countdown{padding:4vh 2.5vw;
2805gap:3vh;border:1px solid rgba(123,211,217,.23);border-radius:5px;background:linear-gradient(140deg,rgba(24,59,77,.7),rgba(9,28,44,.85));box-shadow:0 22px 60px rgba(0,0,0,.2);backdrop-filter:none}
2806.tv-waiting-broadcast .tv-waiting-countdown:before{inset:0 0 auto;height:3px;border:0;border-radius:0;background:linear-gradient(90deg,#77d7d7,#2a748d)}
2807.tv-waiting-broadcast .tv-waiting-countdown-label{font-size:clamp(16px,1.4vw,27px);letter-spacing:.08em;color:#d4e6ef}
2808.tv-waiting-broadcast .tv-waiting-count-grid{gap:0}
2809.tv-waiting-broadcast .tv-waiting-count-part{gap:1.5vh;min-width:0;border-right:1px solid rgba(255,255,255,.12)}
2810.tv-waiting-broadcast .tv-waiting-count-part:last-child{border-right:0}
2811.tv-waiting-broadcast .tv-waiting-count-part strong{min-height:0;padding:1vh 0;background:none;border:0;border-radius:0;box-shadow:none;font-size:clamp(30px,4.3vw,82px);letter-spacing:-.035em;font-weight:800}
2812.tv-waiting-broadcast .tv-waiting-count-part:last-child strong{color:#7bd7d9}
2813.tv-waiting-broadcast .tv-waiting-count-part small{font-size:clamp(12px,1vw,19px);letter-spacing:.04em;color:#c0d6e2}
2814.tv-waiting-broadcast .tv-waiting-local-time{border-top:1px solid rgba(255,255,255,.1);padding-top:2vh;font-size:clamp(15px,1.3vw,25px);color:#b4cad8;flex-wrap:wrap}
2815.tv-waiting-broadcast .tv-waiting-message{font-size:clamp(28px,3vw,58px);animation:none}
2816.tv-waiting-broadcast-footer{display:flex;justify-content:space-between;align-items:center;gap:20px;border-top:1px solid rgba(255,255,255,.18);padding-top:2.5vh;font-size:clamp(14px,1.2vw,23px);letter-spacing:.06em;text-transform:uppercase;color:#b4cad8}
2817.tv-waiting-broadcast-footer>span:first-child{display:flex;align-items:center;gap:10px}
2818.tv-waiting-broadcast-footer i{height:6px;width:6px;border-radius:50%;background:#77d7d7}
2819@media(max-aspect-ratio:1/1){.tv-waiting-broadcast .tv-waiting-frame{height:90vh;gap:3vh}.tv-waiting-broadcast .tv-waiting-body{grid-template-columns:1fr;gap:3vh}.tv-waiting-broadcast .tv-waiting-title{font-size:6vw}.tv-waiting-broadcast .tv-waiting-kicker{margin:0}.tv-waiting-broadcast .tv-waiting-count-part strong{font-size:8vw}.tv-waiting-broadcast-footer{letter-spacing:.02em}}
2820@media (prefers-reduced-motion:reduce){.tv-waiting-screen,.tv-waiting-screen:before,.tv-waiting-screen:after,.tv-waiting-orb,.tv-waiting-logo,.tv-waiting-message{animation:none!important}}
2821.mailing-editor-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:9px;border:1px solid #cfdbe8;border-bottom:0;border-radius:10px 10px 0 0;background:#f4f8fb}.mailing-editor-tool{height:32px;min-width:32px;padding:0 8px;border:1px solid #cad8e5;border-radius:7px;background:#fff;color:#173a5d;font:800 11px inherit;cursor:pointer}.mailing-editor-tool:hover{border-color:#0a5ca8;background:#edf6fd}.mailing-editor-tool:disabled{opacity:.45;cursor:not-allowed}.mailing-editor-separator{width:1px;height:24px;background:#cfdae5;margin:0 2px}.mailing-rich-editor{min-height:280px;max-height:560px;overflow:auto;border:1px solid #cfdbe8;border-radius:0 0 10px 10px;padding:22px;background:#fff;color:#172b3f;font:15px/1.65 Arial,sans-serif;outline:none}.mailing-rich-editor:focus{border-color:#0a5ca8;box-shadow:inset 0 0 0 1px #0a5ca8}.mailing-rich-editor:empty:before{content:attr(data-placeholder);color:#8a99a9;pointer-events:none}.mailing-rich-editor img{max-width:100%;height:auto}.mailing-rich-editor a{color:#075ca8}.mailing-editor-is-expanded body{overflow:hidden!important}.mailing-editor-is-expanded section:has(.mailing-editor-fullscreen){transform:none!important}.mailing-editor-fullscreen{position:fixed!important;inset:18px!important;z-index:9998!important;margin:0!important;padding:18px!important;border-radius:16px!important;background:#fff!important;box-shadow:0 24px 80px rgba(0,20,45,.35)!important;overflow:auto!important}.mailing-editor-fullscreen .mailing-rich-editor{min-height:calc(100vh - 210px);max-height:none}.mailing-editor-dialog-backdrop{position:fixed;inset:0;z-index:10020;background:rgba(4,24,45,.64);display:grid;place-items:center;padding:20px}.mailing-editor-dialog{width:min(560px,100%);max-height:90vh;overflow:auto;border-radius:16px;background:#fff;box-shadow:0 24px 80px rgba(0,0,0,.28);padding:20px}.mailing-editor-source{width:100%;min-height:320px;resize:vertical;border:1px solid #cfdbe8;border-radius:0 0 10px 10px;padding:14px;box-sizing:border-box;background:#0c1b2a;color:#d7e5f2;font:12px/1.55 ui-monospace,Consolas,monospace;outline:none}
2822.mailing-editor-is-expanded section:has(.mailing-editor-fullscreen){overflow:visible!important}
2823.tv-video-portal{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
2824.tv-video-portal.is-prewarming{opacity:0;visibility:hidden}
2825.tv-video-bg{position:absolute;inset:0;z-index:0;pointer-events:none;background:radial-gradient(circle at 16% 20%,rgba(42,199,210,.58),transparent 30%),radial-gradient(circle at 88% 28%,rgba(0,88,178,.72),transparent 34%),linear-gradient(118deg,#001d3f 0%,#003e76 46%,#16acbb 100%);background-size:150% 150%;animation:tvVideoGlow 12s ease-in-out infinite alternate}
2826@keyframes tvVideoGlow{0%{background-position:0% 30%;filter:saturate(1)}100%{background-position:100% 70%;filter:saturate(1.12)}}
2827.tv-video-layer{position:absolute;z-index:1;pointer-events:none}
2828.tv-video-layer.is-single{inset:0;display:block}
2829.tv-video-layer.is-grid{left:2vw;right:2vw;top:max(18vh,calc(4.2vh + 131px));
2829bottom:max(30vh,calc(4.4vh + 196px));display:grid;gap:clamp(12px,1.35vw,24px);align-items:center;justify-content:center}
2830.tv-video-layer.is-grid.is-without-lower-third{bottom:2vw}
2831.tv-video-layer.is-grid.is-count-1{grid-template-columns:minmax(0,min(54vw,900px))}
2832.tv-video-layer.is-grid.is-count-2{grid-template-columns:repeat(2,minmax(0,1fr));width:96vw;margin-inline:auto}
2833.tv-video-layer.is-grid.is-count-3{grid-template-columns:repeat(3,minmax(0,1fr));width:96vw;margin-inline:auto}
2834.tv-video-layer.is-grid.is-count-4{left:50%;right:auto;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));width:auto;max-width:96vw;aspect-ratio:16/9;transform:translateX(-50%)}
2835.rosgan-remate-admin-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
2836.rg-active-remates{align-items:start}
2837.rg-active-remates article{padding:13px!important;border-radius:15px!important}
2838.rg-active-remates article>div:first-child{gap:7px!important}
2839.rg-active-remates article>div:first-child>span{width:34px!important;height:34px!important;border-radius:10px!important}
2840.rg-active-remates article>div:first-child>span img{width:28px!important;height:28px!important;object-fit:contain}
2841.rg-active-remates .remate-card-facts{margin:10px 0 0!important;gap:6px!important}
2842.rg-active-remates .remate-card-facts>div{padding:7px 9px!important;border-radius:9px!important}
2843.remate-card-facts{grid-template-columns:repeat(2,minmax(0,1fr))!important}
2844.remate-card-facts>div:last-child:nth-child(3){grid-column:1/-1}
2845.remate-card-facts strong{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;overflow-wrap:anywhere;line-height:1.22}
2846.remate-card-consignataria{display:flex;align-items:center;gap:7px;margin-top:3px}
2847.remate-card-consignataria-logo{width:24px;height:24px;border:1px solid #dce6f0;border-radius:7px;background:#fff;display:grid;place-items:center;flex:none;overflow:hidden}
2848.remate-card-consignataria-logo img{width:20px;height:20px;object-fit:contain}
2849.remate-card-header-cons-logo{width:45px;height:45px;border:1px solid #dce6f0;border-radius:13px;background:#fff;display:grid;place-items:center;flex:none;overflow:hidden}
2850.remate-card-header-cons-logo img{width:36px;height:36px;object-fit:contain}
2851@media(max-width:1050px){.rosgan-remate-admin-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
2852@media(max-width:760px){.rosgan-remate-admin-grid{grid-template-columns:minmax(0,1fr)}}
2853/* Remates: compact list shared by active, upcoming and archive sections. */
2854.rosgan-remate-admin-grid{grid-template-columns:minmax(0,1fr);gap:0;overflow:hidden;border:1px solid #e2eaf3;border-radius:15px;background:#fff;box-shadow:0 6px 20px rgba(0,30,60,.035)}
2855.rosgan-remate-admin-grid article{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) 16px;align-items:center;gap:24px;padding:15px 18px!important;border:0!important;border-bottom:1px solid #eaf0f6!important;border-radius:0!important;box-shadow:none!important;background:transparent!important;transition:background .18s!important}
2856.rosgan-remate-admin-grid article:last-child{border-bottom:0!important}
2857.rosgan-remate-admin-grid article:hover{background:#f5f9fd!important;transform:none!important}
2858.rosgan-remate-admin-grid article:focus-visible{outline:2px solid #0a5ca8;outline-offset:-3px}
2859.rosgan-remate-admin-grid article::after{content:'›';font-size:24px;font-weight:400;color:#7b95ad;text-align:right}
2860.rosgan-remate-admin-grid article>div:first-child{display:grid!important;grid-template-columns:38px 38px minmax(0,1fr);align-items:center!important;gap:10px!important}
2861.rosgan-remate-admin-grid article>div:first-child>span:first-child{grid-column:1;grid-row:1}
2862.rosgan-remate-admin-grid article>div:first-child>.remate-card-header-cons-logo{grid-column:2;grid-row:1}
2863.rosgan-remate-admin-grid article>div:first-child>div{grid-column:3;grid-row:1}
2864.rosgan-remate-admin-grid article>div:first-child>span{width:38px!important;height:38px!important;border-radius:10px!important}
2865.rosgan-remate-admin-grid article>div:first-child>span img{max-width:30px;max-height:30px;object-fit:contain}
2866.rosgan-remate-admin-grid article h3{font-size:15px!important;line-height:1.3!important;margin:3px 0!important}
2867.rosgan-remate-admin-grid .remate-card-facts{margin:0!important;gap:16px!important}
2868.rosgan-remate-admin-grid .remate-card-facts>div{background:transparent!important;border-radius:0!important;padding:0 0 0 14px!important;border-left:1px solid #e7edf4}
2869.rosgan-remate-admin-grid .remate-card-facts strong{font-size:12px!important}
2870.rosgan-remate-admin-grid .remate-card-facts small{margin-bottom:4px}
2871@media(max-width:760px){
2872.rosgan-remate-admin-grid article{grid-template-columns:minmax(0,1fr) 14px;gap:12px;padding:14px!important}
2873.rosgan-remate-admin-grid article>div:first-child{grid-column:1;grid-row:1}
2874.rosgan-remate-admin-grid article::after{grid-column:2;grid-row:1/3;align-self:center}
2875.rosgan-remate-admin-grid .remate-card-facts{grid-column:1;grid-row:2;gap:12px!important}
2876.rosgan-remate-admin-grid .remate-card-facts>div{padding-left:0!important;border-left:0}
2877}
2878.rosgan-remate-admin-grid article{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) 160px 16px;gap:16px}
2879.rosgan-remate-admin-grid article::after{grid-column:4;grid-row:1}
2880.remate-list-catalog{grid-column:3;grid-row:1}
2881.remate-list-catalog{position:relative;overflow:hidden;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 12px;border:1px solid #16856b;border-radius:99px;background:linear-gradient(115deg,#08624e,#108366);color:#fff;font-family:inherit;font-size:11px;font-weight:750;white-space:nowrap;cursor:pointer;box-shadow:0 4px 12px rgba(8,98,78,.22);animation:remateCatalogGlow 3.6s ease-in-out infinite;transition:transform .2s,filter .2s}
2882.remate-list-catalog::before{content:"";position:absolute;inset:-50% auto -50% -60%;width:38%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.28),transparent);transform:skewX(-22deg);pointer-events:none;animation:remateCatalogShine 4.8s ease-in-out infinite}
2883.remate-list-catalog img{width:14px;
2883height:14px;filter:brightness(0) invert(1)}
2884.remate-list-catalog:hover{filter:brightness(1.12);transform:translateY(-2px)}
2885.remate-list-catalog:active{transform:translateY(0)}
2886@keyframes remateCatalogGlow{0%,100%{box-shadow:0 4px 12px rgba(8,98,78,.22),0 0 0 0 rgba(16,131,102,0)}50%{box-shadow:0 5px 18px rgba(8,98,78,.3),0 0 0 4px rgba(16,131,102,.1)}}
2887@keyframes remateCatalogShine{0%,65%{left:-60%}90%,100%{left:130%}}
2888@media(prefers-reduced-motion:reduce){.remate-list-catalog,.remate-list-catalog::before{animation:none;transition:none}.remate-list-catalog:hover{transform:none}}
2889.remate-list-catalog:focus-visible{outline:2px solid #0a5ca8;outline-offset:3px}
2890@media(max-width:760px){.rosgan-remate-admin-grid article{grid-template-columns:minmax(0,1fr) 14px;gap:12px}.rosgan-remate-admin-grid article::after{grid-column:2;grid-row:1/3}.remate-list-catalog{grid-column:1;grid-row:3;justify-self:start}}
2891
2892.pc-directory-tools{display:grid;grid-template-columns:minmax(220px,2fr) repeat(2,minmax(160px,1fr));gap:12px;background:#fff;border:1px solid #e1e9f2;border-radius:16px;padding:16px}
2893.pc-directory-tools label{display:grid;gap:7px;min-width:0;font-size:12px;font-weight:700;color:#60758c}
2894.pc-directory-tools input,.pc-directory-tools select{box-sizing:border-box;width:100%;min-width:0;min-height:44px;padding:10px 12px;border:1px solid #cfdeeb;border-radius:10px;background:#f8fafd;color:#102f50;font:inherit;font-size:14px;font-weight:400}
2895.pc-directory-tools :is(input,select):focus-visible,.pc-directory-row button:focus-visible{outline:2px solid #0a5ca8;outline-offset:2px}
2896.pc-directory-summary{display:flex;align-items:center;justify-content:space-between;min-height:50px;color:#60758c;font-size:13px}
2897.pc-directory-summary button,.pc-directory-row button,.pc-directory-pages button{min-height:40px;padding:8px 13px;border:1px solid #d2e1ef;border-radius:9px;background:#f7faff;color:#075da8;font:600 12px inherit;cursor:pointer}
2898.pc-directory-list{list-style:none;padding:0;margin:0;border:1px solid #e1e9f2;border-radius:16px;background:#fff;overflow:hidden}
2899.pc-directory-row{display:grid;grid-template-columns:minmax(0,2fr) minmax(120px,.8fr) minmax(0,1.3fr) auto;gap:20px;align-items:center;padding:17px 20px;border-bottom:1px solid #eaf0f6}
2900.pc-directory-row:last-child{border-bottom:0}.pc-directory-row:hover{background:#f8fbfe}.pc-directory-row>div{display:grid;gap:5px;min-width:0;overflow-wrap:anywhere}.pc-directory-name strong{font-size:15px;color:#102f50}.pc-directory-name span{font-size:12px;color:#6d8198}.pc-directory-row small{font-size:10px;color:#8593a6;text-transform:uppercase;letter-spacing:.05em}.pc-directory-cuit span,.pc-directory-location span{font-size:13px;color:#3d536c}.pc-directory-empty{padding:40px 20px;text-align:center;border:1px dashed #cfdbe9;border-radius:14px;color:#60758c}.pc-directory-pages{display:flex;gap:16px;align-items:center;justify-content:center;margin-top:18px;font-size:13px;color:#60758c}.pc-directory-pages button:disabled{opacity:.4;cursor:default}
2901@media(max-width:760px){.pc-directory-tools{grid-template-columns:1fr 1fr;padding:12px;gap:10px}.pc-search{grid-column:1/-1}.pc-directory-tools input,.pc-directory-tools select{font-size:16px}.pc-directory-row{grid-template-columns:1fr 1fr;padding:16px;gap:12px}.pc-directory-name{grid-column:1/-1}.pc-directory-row button{grid-column:1/-1;justify-self:start;min-height:44px}.pc-directory-name strong{font-size:16px}}
2902
2903.remate-list-catalog.rd-catalog-cta{grid-column:auto;grid-row:auto;min-height:44px;padding:12px 20px;gap:8px;font-size:13px;flex-shrink:0}
2904@keyframes companyCatalogGlow{0%,100%{box-shadow:0 4px 12px color-mix(in srgb,var(--company-color,#002e5f) 25%,transparent)}50%{box-shadow:0 4px 22px color-mix(in srgb,var(--company-color,#002e5f) 50%,transparent)}}.rd-company-catalog{animation-name:companyCatalogGlow;background:linear-gradient(115deg,var(--company-color,#002e5f),var(--company-color,#002e5f))!important;border-color:var(--company-color,#002e5f)!important}.rd-company-dialog{width:min(480px,calc(100% - 32px));box-sizing:border-box;border:1px solid #dce4ef;border-radius:18px;padding:26px;color:#002e5f}.rd-company-dialog::backdrop{background:rgba(0,29,61,.55)}.rd-company-dialog h2{font-size:21px;margin:0 0 22px}.rd-company-dialog label{display:grid;gap:8px;font-weight:700}.rd-company-dialog select{width:100%;padding:12px;border:1px solid #c6d8e9;border-radius:8px;font:inherit}.rd-company-dialog p{font-size:14px;line-height:1.5;color:#62748a}.rd-company-dialog-actions{display:flex;gap:10px;flex-wrap:wrap}.rd-company-dialog-actions button{flex:1;padding:14px;border:0;border-radius:9px;background:var(--company-color,#002e5f);color:white;font-weight:700;cursor:pointer}.rd-company-dialog-close{margin-top:18px;padding:10px 0;border:0;background:none;color:#002e5f;cursor:pointer}.rd-catalog-cta img{width:17px;height:17px}
2905.roslot-pool-page{box-sizing:border-box}.roslot-ops-shell{display:block;margin:0 0 16px}
2906.roslot-ops-panel,.roslot-live-tower{min-width:0;border:1px solid #dce6f0;border-radius:18px;background:rgba(255,255,255,.96);box-shadow:0 12px 34px rgba(0,30,60,.07)}
2907.roslot-ops-panel{padding:17px}.roslot-live-tower{position:static;overflow:hidden}
2908.roslot-ops-head,.roslot-live-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.roslot-ops-head{margin-bottom:9px}.roslot-ops-title small,.roslot-live-title small{display:block;color:#0a5ca8;font-size:9px;font-weight:950;letter-spacing:.13em;text-transform:uppercase}.roslot-ops-title h2,.roslot-live-title h2{margin:3px 0 0;color:#112033;font-size:18px;line-height:1.1}
2908.roslot-role-chips{display:flex;justify-content:flex-end;gap:5px;flex-wrap:wrap}.roslot-role-chip{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:#edf4fb;color:#35516d;font-size:9px;font-weight:900;text-transform:uppercase}.roslot-role-chip:before{content:"";width:6px;height:6px;border-radius:50%;background:#1f9d78}
2909.roslot-whatsapp-control{display:flex;align-items:center;gap:11px;min-width:238px;margin:-3px 0 0;padding:7px 8px 7px 10px;border:1px solid #d7e3ed;border-radius:13px;background:#f6f9fc;color:#173958;font-family:'Archivo',sans-serif;text-align:left;cursor:pointer;transition:border-color .18s,background .18s,box-shadow .18s,transform .18s}.roslot-whatsapp-control:hover{transform:translateY(-1px);border-color:#a8c8bd;box-shadow:0 7px 18px rgba(0,60,45,.08)}.roslot-whatsapp-control:disabled{cursor:wait;opacity:.62;transform:none}.roslot-whatsapp-copy{display:grid;grid-template-columns:29px minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;min-width:0;flex:1}.roslot-whatsapp-copy>span{grid-row:1/-1;display:grid;place-items:center;width:29px;height:29px;border-radius:9px;background:#e4f5ee}.roslot-whatsapp-copy img{width:17px;height:17px}.roslot-whatsapp-copy strong{overflow:hidden;color:#173958;font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap}.roslot-whatsapp-copy small{margin-top:2px;color:#738499;font-size:8.5px;font-weight:750}.roslot-whatsapp-switch{position:relative;width:36px;height:21px;flex:0 0 36px;border-radius:999px;background:#aebbc8;box-shadow:inset 0 0 0 1px rgba(0,30,60,.08);transition:background .2s}.roslot-whatsapp-switch i{position:absolute;top:3px;left:3px;width:15px;height:15px;border-radius:50%;background:#fff;box-shadow:0 2px 5px rgba(0,30,60,.22);transition:transform .2s}.roslot-whatsapp-control.is-on{border-color:#9dd8c4;background:#f0fbf7}.roslot-whatsapp-control.is-on .roslot-whatsapp-copy>span{background:#d9f6ea}.roslot-whatsapp-control.is-on .roslot-whatsapp-copy small{color:#177258}.roslot-whatsapp-control.is-on .roslot-whatsapp-switch{background:#1f9d78}.roslot-whatsapp-control.is-on .roslot-whatsapp-switch i{transform:translateX(15px)}.roslot-whatsapp-control.is-unavailable{border-style:dashed}.roslot-whatsapp-control.is-unavailable .roslot-whatsapp-copy small{color:#a26020}
2910.roslot-day-summary{margin-top:10px;padding-top:11px;border-top:1px solid #e5edf5}.roslot-day-summary-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.roslot-day-summary-head strong{color:#4d6175;font-size:9px;font-weight:950;letter-spacing:.12em;text-transform:uppercase}.roslot-day-summary-head span{color:#8593a6;font-size:9px;font-weight:750}.roslot-day-category-list{display:flex;flex-wrap:wrap;gap:6px}.roslot-day-category{display:inline-grid;grid-template-columns:auto auto;align-items:baseline;gap:1px 5px;padding:6px 9px;border:1px solid #dce6f0;border-radius:10px;background:#f5f8fc;color:#173958}.roslot-day-category strong{font-size:15px;font-weight:950;line-height:1}.roslot-day-category i{font-size:8px;font-style:normal;font-weight:850;text-transform:uppercase}.roslot-day-category em{grid-column:1/-1;overflow:hidden;max-width:210px;color:#65758a;font-size:9px;font-style:normal;font-weight:750;text-overflow:ellipsis;white-space:nowrap}.roslot-day-summary-empty{color:#8593a6;font-size:10px;font-weight:700}
2911.roslot-live-state{display:inline-flex;align-items:center;gap:6px;flex:none;padding:6px 9px;border-radius:999px;background:#e5f5ef;color:#16725c;font-size:9px;font-weight:950;text-transform:uppercase}.roslot-live-state i{width:7px;height:7px;border-radius:50%;background:#1f9d78;box-shadow:0 0 0 0 rgba(31,157,120,.4);animation:livePing 1.7s ease-out infinite}
2912.roslot-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:14px 0}.roslot-kpi{min-width:0;padding:11px;border:1px solid #e4ebf3;border-radius:12px;background:#f7f9fc}.roslot-kpi small{display:block;overflow:hidden;color:#738499;font-size:9px;font-weight:850;letter-spacing:.04em;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase}.roslot-kpi strong{display:block;margin-top:5px;color:#112033;font-size:20px;font-variant-numeric:tabular-nums}.roslot-kpi em{display:block;overflow:hidden;margin-top:2px;color:#65758a;font-size:9px;font-style:normal;font-weight:700;text-overflow:ellipsis;white-space:nowrap}
2913.roslot-op-list{display:grid;gap:7px}.roslot-op-row{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;padding:10px 11px;border:1px solid #e3eaf2;border-radius:12px;background:#fff;transition:border-color .18s,box-shadow .18s}.roslot-op-row:hover{border-color:#bad0e6;box-shadow:0 8px 20px rgba(0,46,95,.07)}.roslot-op-row.is-outbid{overflow:hidden;border:2px solid #e63946;background:linear-gradient(105deg,#fff0f2 0%,#fff 68%);box-shadow:0 0 0 3px rgba(230,57,70,.1),0 12px 30px rgba(181,29,44,.18);animation:roslotOutbidAttention 1.8s ease-in-out infinite}.roslot-op-row.is-outbid:before{content:"";position:absolute;inset:0 auto 0 0;width:5px;background:#e63946}.roslot-op-copy{min-width:0}.roslot-op-topline{display:flex;align-items:center;gap:7px;min-width:0}.roslot-op-kind{flex:none;padding:3px 6px;border-radius:999px;font-size:8px;font-weight:950;letter-spacing:.05em;text-transform:uppercase}.roslot-op-copy strong{display:block;overflow:hidden;color:#112033;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.roslot-op-copy p{overflow:hidden;margin:4px 0 0;color:#65758a;font-size:10px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.roslot-op-alert{display:flex;align-items:center;gap:6px;width:fit-content;margin:8px 0 6px;padding:6px 9px;border-radius:8px;background:#d91f32;color:#fff;box-shadow:0 6px 15px rgba(191,28,44,.24)}.roslot-op-alert i{font-size:14px}.roslot-op-alert strong{color:#fff;font-size:11px;letter-spacing:.05em}.roslot-op-alert span{font-size:9px;font-weight:750;opacity:.9}.roslot-op-price{display:flex;align-items:baseline;gap:7px;width:fit-content;margin-top:7px;padding:7px 10px;border:1px solid #d6e3ef;border-radius:10px;background:#f4f8fc}.roslot-op-price span{color:#65758a;font-size:8px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.roslot-op-price strong{overflow:visible;color:#003d78;font-size:23px;line-height:1;font-weight:950;font-variant-numeric:tabular-nums}
2913.roslot-op-price em{color:#65758a;font-size:9px;font-style:normal;font-weight:750}.roslot-op-row.is-outbid .roslot-op-price{border-color:#f1b6bd;background:#fff}.roslot-op-row.is-outbid .roslot-op-price strong{color:#c51f31}.roslot-op-status{display:inline-flex;align-items:center;gap:5px;margin-top:7px;font-size:9px;font-weight:900}.roslot-op-status:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}.roslot-op-actions{display:flex;justify-content:flex-end;gap:5px;flex-wrap:wrap}.roslot-op-actions button{min-height:30px;border:1px solid #d7e2ed;border-radius:9px;background:#fff;color:#173958;padding:6px 9px;font:850 10px 'Archivo',sans-serif;cursor:pointer}.roslot-op-actions button.is-primary{border-color:#005ca8;background:#005ca8;color:#fff}.roslot-op-row.is-outbid .roslot-op-actions button{border-color:#e63946;color:#bc2031}.roslot-op-row.is-outbid .roslot-op-actions button.is-primary{background:#e63946;color:#fff}.roslot-op-actions button.is-success{border-color:#168064;background:#168064;color:#fff}.roslot-op-actions button.is-danger{border-color:#f0c3c8;background:#fff4f5;color:#bd2c39}.roslot-op-actions button:disabled{cursor:wait;opacity:.48}.roslot-ops-empty{padding:18px;border:1px dashed #cbd7e4;border-radius:12px;color:#65758a;font-size:11px;font-weight:750;text-align:center}@keyframes roslotOutbidAttention{0%,100%{box-shadow:0 0 0 3px rgba(230,57,70,.08),0 12px 30px rgba(181,29,44,.14)}50%{box-shadow:0 0 0 6px rgba(230,57,70,.2),0 14px 34px rgba(181,29,44,.26)}}@media(prefers-reduced-motion:reduce){.roslot-op-row.is-outbid{animation:none}}
2914.roslot-live-head{padding:15px;border-bottom:1px solid #e7edf4}.roslot-live-list{display:grid;max-height:174px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}.roslot-live-row{display:grid;grid-template-columns:32px minmax(0,1fr);gap:9px;padding:11px 13px;border:0;border-bottom:1px solid #edf1f5;background:#fff;text-align:left;cursor:pointer}.roslot-live-row:hover{background:#f7faff}.roslot-live-icon{width:32px;height:32px;display:grid;place-items:center;border-radius:10px}.roslot-live-icon img{width:15px;height:15px}.roslot-live-copy{min-width:0}.roslot-live-copy strong{display:block;overflow:hidden;color:#112033;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.roslot-live-copy p{display:-webkit-box;overflow:hidden;margin:3px 0 0;color:#65758a;font-size:9.5px;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}.roslot-live-meta{display:flex;justify-content:space-between;gap:8px;margin-top:5px;color:#8291a3;font-size:8.5px;font-weight:800}.roslot-live-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 13px;background:#f7f9fc;color:#65758a;font-size:9px;font-weight:800}.roslot-live-footer button{border:0;background:transparent;color:#075ca8;font:900 10px 'Archivo',sans-serif;cursor:pointer}.roslot-live-empty{padding:26px 16px;color:#718196;font-size:11px;font-weight:750;text-align:center}
2915.roslot-mobile-panel-head,.roslot-mobile-dock{display:none}.roslot-event-toast{position:fixed;z-index:96;left:14px;right:14px;bottom:calc(82px + env(safe-area-inset-bottom));display:grid;grid-template-columns:38px minmax(0,1fr) 16px;gap:10px;align-items:center;width:calc(100% - 28px);max-width:440px;margin:auto;padding:11px 12px;border:1px solid rgba(255,255,255,.2);border-radius:16px;background:linear-gradient(135deg,#00366c,#005ca8);color:#fff;box-shadow:0 18px 42px rgba(0,30,60,.3);text-align:left;font-family:'Archivo',sans-serif;cursor:pointer;animation:roslotToastIn .3s cubic-bezier(.22,.72,.18,1) both}.roslot-event-toast-icon{width:38px;height:38px;display:grid;place-items:center;border-radius:12px;background:rgba(255,255,255,.14)}.roslot-event-toast-icon img{width:18px;height:18px;filter:brightness(0) invert(1)}.roslot-event-toast-copy{min-width:0}.roslot-event-toast-copy strong{display:block;overflow:hidden;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.roslot-event-toast-copy span{display:-webkit-box;overflow:hidden;margin-top:3px;color:#dcecff;font-size:10px;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}.roslot-event-toast-arrow{font-size:18px;font-weight:900}@keyframes roslotToastIn{from{opacity:0;transform:translateY(24px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes roslotPanelIn{from{opacity:.35;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}
2916.roslot-live-row.is-arriving{position:relative;z-index:1;animation:roslotLiveArrival 1.1s cubic-bezier(.22,.72,.18,1) both}.roslot-live-row.is-arriving .roslot-live-icon{animation:roslotLiveIconPulse 1.1s ease-out both}@keyframes roslotLiveArrival{0%{opacity:0;transform:translateX(24px);background:#cfe9ff;box-shadow:inset 4px 0 #0a5ca8}55%{opacity:1;transform:translateX(0);background:#e8f5ff;box-shadow:inset 4px 0 #0a5ca8}100%{background:#fff;box-shadow:inset 0 0 transparent}}@keyframes roslotLiveIconPulse{0%,100%{transform:scale(1)}45%{transform:scale(1.18);box-shadow:0 0 0 7px rgba(10,92,168,.12)}}
2917@media(min-width:1181px){.roslot-pool-page{padding-right:clamp(370px,28vw,420px)!important}.roslot-live-tower{position:fixed;z-index:42;top:78px;right:clamp(14px,4vw,36px);width:clamp(320px,24vw,370px);max-height:calc(100vh - 96px)}.roslot-live-list{max-height:calc(100vh - 190px)}}
2918@media(max-width:1180px){.roslot-pool-page{padding-right:clamp(14px,4vw,36px)!important}.roslot-ops-shell{display:grid;grid-template-columns:minmax(0,1fr);gap:14px}.roslot-live-list{grid-template-columns:repeat(2,minmax(0,1fr))}.roslot-live-row:nth-child(odd):last-child{grid-column:1/-1}}
2919@media(max-width:760px){.roslot-ops-shell{display:none;position:fixed;z-index:88;inset:58px 0 calc(67px + env(safe-area-inset-bottom));gap:10px;margin:0;padding:14px;overflow-y:auto;overscroll-behavior:contain;background:#f4f7fb}.roslot-ops-shell.is-mobile-open{display:grid;animation:roslotPanelIn .28s cubic-bezier(.22,.72,.18,1) both}.roslot-mobile-panel-head{position:sticky;z-index:3;top:-14px;display:flex;align-items:center;justify-content:space-between;gap:12px;margin:-14px -14px 0;padding:13px 15px;border-bottom:1px solid #dce6f0;background:rgba(244,247,251,.96);backdrop-filter:blur(12px)}.roslot-mobile-panel-head strong{color:#112033;font-size:16px}.roslot-mobile-panel-head button{width:36px;height:36px;display:grid;place-items:center;border:1px solid #d5e0eb;border-radius:11px;background:#fff;color:#173958;cursor:pointer}.roslot-mobile-dock{position:fixed;z-index:57;top:50%;right:0;display:flex;align-items:center;gap:7px;transform:translateY(-50%);padding:12px 8px;border:0;border-radius:14px 0 0 14px;background:#003d78;color:#fff;box-shadow:0 10px 26px rgba(0,36,75,.25);font:850 10px 'Archivo',sans-serif;letter-spacing:.04em;writing-mode:vertical-rl;cursor:pointer}.roslot-mobile-dock.is-hidden{display:none}.roslot-mobile-dock img{width:16px;height:16px;filter:brightness(0) invert(1);transform:rotate(90deg)}.roslot-ops-panel{padding:13px;border-radius:15px}.roslot-ops-head{align-items:center}
2919.roslot-role-chips{display:none}.roslot-kpis{grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin:11px 0}.roslot-kpi{padding:9px}.roslot-kpi strong{font-size:18px}.roslot-op-row{grid-template-columns:minmax(0,1fr);gap:8px}.roslot-op-price{width:auto;justify-content:flex-start}.roslot-op-price strong{font-size:26px}.roslot-op-alert{width:auto}.roslot-op-actions{justify-content:flex-start}.roslot-op-actions button{flex:1;min-width:72px}.roslot-day-category{flex:1 1 120px}.roslot-day-category em{max-width:155px}.roslot-live-list{grid-template-columns:minmax(0,1fr);max-height:none}.roslot-live-row:nth-child(odd):last-child{grid-column:auto}}
2920@media(max-width:760px){.roslot-ops-head{align-items:flex-start;flex-direction:column}.roslot-whatsapp-control{width:100%;min-width:0;margin:2px 0 0;box-sizing:border-box}.roslot-whatsapp-copy strong{font-size:10.5px}}
2921.roslot-price-badge{transition:background .25s,border-color .25s,box-shadow .25s}.roslot-price-badge.has-offers{background:rgba(13,132,92,.88)!important;border-color:rgba(155,255,216,.76)!important;box-shadow:0 0 0 1px rgba(61,221,164,.18),0 0 18px rgba(29,202,139,.38);animation:roslotOfferLive 2.2s ease-in-out infinite}.roslot-price-live-dot{width:6px;height:6px;flex:none;border-radius:50%;background:#baffdf;box-shadow:0 0 0 0 rgba(186,255,223,.7);animation:roslotOfferDot 1.35s ease-out infinite}@keyframes roslotOfferLive{0%,100%{box-shadow:0 0 0 1px rgba(61,221,164,.18),0 0 12px rgba(29,202,139,.24)}50%{box-shadow:0 0 0 2px rgba(121,255,207,.35),0 0 24px rgba(29,202,139,.52)}}@keyframes roslotOfferDot{0%{box-shadow:0 0 0 0 rgba(186,255,223,.75)}75%,100%{box-shadow:0 0 0 7px rgba(186,255,223,0)}}.roslot-op-copy strong{display:-webkit-box;overflow:hidden;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}
2922.roslot-report-entry{display:inline-flex;align-items:center;gap:7px;padding:9px 15px;border:1px solid #c8d8e7;border-radius:100px;background:#fff;color:#063d72;font:800 12.5px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 5px 14px rgba(0,46,95,.06);transition:transform .16s,border-color .16s,box-shadow .16s}.roslot-report-entry:hover{transform:translateY(-2px);border-color:#73a2ca;box-shadow:0 9px 20px rgba(0,46,95,.12)}
2923.roslot-presence-strip{display:inline-flex;align-items:center;gap:6px;margin-top:9px;padding:5px;border:1px solid #d7e4ee;border-radius:16px;background:rgba(255,255,255,.94);box-shadow:0 7px 18px rgba(0,46,95,.07);color:#38546f;font-size:10px;font-weight:850;line-height:1}.roslot-presence-live,.roslot-presence-role{display:inline-flex;align-items:center;white-space:nowrap}.roslot-presence-live{gap:6px;min-height:32px;padding:0 10px;border-radius:11px;background:#e4f5ef;color:#16725c;font-size:8px;letter-spacing:.08em;text-transform:uppercase}.roslot-presence-live i{width:7px;height:7px;border-radius:50%;background:#1f9d78;box-shadow:0 0 0 0 rgba(31,157,120,.4);animation:livePing 1.7s ease-out infinite}.roslot-presence-role{gap:7px;min-height:32px;padding:3px 10px 3px 4px;border:1px solid #dfeaf4;border-radius:11px;background:#f2f7fc}.roslot-presence-role-icon{display:grid;place-items:center;width:26px;height:26px;flex:0 0 26px;border-radius:9px;background:#dcecff;color:#075ca8;font-size:13px}.roslot-presence-role-copy{display:flex;align-items:baseline;gap:4px}.roslot-presence-role strong{color:#073e73;font-size:13px;font-variant-numeric:tabular-nums}.roslot-presence-role small{color:#4d647a;font-size:9.5px;font-weight:800}.roslot-presence-role.is-seller{border-color:#d9ece5;background:#f0f8f5}.roslot-presence-role.is-seller .roslot-presence-role-icon{background:#dcefe8;color:#14745f}.roslot-presence-role.is-seller strong{color:#14745f}
2924@media(max-width:520px){.roslot-presence-strip{display:flex;width:max-content;max-width:100%;flex-wrap:wrap}.roslot-presence-live{min-height:30px;padding:0 8px}.roslot-presence-role{min-height:30px;padding-right:8px}.roslot-presence-role-icon{width:24px;height:24px;flex-basis:24px}}
2925.roslot-report-page{box-sizing:border-box;width:min(1240px,calc(100% - 32px));margin:0 auto;padding:24px 0 54px;color:#112033;animation:pageIn 420ms cubic-bezier(.22,.72,.18,1)}.roslot-report-header{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}.roslot-report-back{display:inline-flex;align-items:center;gap:6px;margin:0 0 16px;padding:0;border:0;background:transparent;color:#075ca8;font:800 12px 'Archivo',sans-serif;cursor:pointer}.roslot-report-back:hover{text-decoration:underline}.roslot-report-eyebrow{color:#e63946;font-size:10px;font-weight:950;letter-spacing:.16em}.roslot-report-header h1{margin:5px 0 5px;font-size:clamp(27px,3.4vw,42px);line-height:1.02;letter-spacing:-.035em}.roslot-report-header>div:first-child>p{max-width:700px;margin:0;color:#65758a;font-size:13px;line-height:1.55}.roslot-report-date-control{min-width:300px;padding:12px;border:1px solid #dce6f0;border-radius:15px;background:#fff;box-shadow:0 9px 25px rgba(0,30,60,.06)}.roslot-report-date-control>label{display:block;margin:0 0 6px;color:#65758a;font-size:9px;font-weight:900;letter-spacing:.09em;text-transform:uppercase}.roslot-report-date-control>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px}.roslot-report-date-control input,.roslot-report-date-control button{min-height:38px;box-sizing:border-box;border:1px solid #d7e2ed;border-radius:10px;background:#f7f9fc;color:#173958;font:750 12px 'Archivo',sans-serif}.roslot-report-date-control input{min-width:0;padding:0 10px}.roslot-report-date-control button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 12px;background:#075ca8;color:#fff;border-color:#075ca8;cursor:pointer}
2926.roslot-report-hero{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1.2fr) minmax(240px,.8fr) minmax(260px,.85fr);gap:28px;align-items:center;
2926padding:28px 30px;border-radius:24px;background:linear-gradient(125deg,#002f61 0%,#064d89 56%,#087e87 100%);color:#fff;box-shadow:0 20px 46px rgba(0,49,96,.2)}.roslot-report-hero:after{content:"";position:absolute;right:-80px;bottom:-140px;width:360px;height:360px;border:1px solid rgba(255,255,255,.14);border-radius:50%;box-shadow:0 0 0 44px rgba(255,255,255,.025),0 0 0 88px rgba(255,255,255,.02)}.roslot-report-hero-copy,.roslot-report-hero-date,.roslot-report-hero-note{position:relative;z-index:1}.roslot-report-hero-copy>span{font-size:10px;font-weight:900;letter-spacing:.13em;text-transform:uppercase;color:#8ee6d6}.roslot-report-hero-copy h2{margin:7px 0 4px;font-size:clamp(30px,4vw,50px);line-height:1;font-variant-numeric:tabular-nums;letter-spacing:-.04em}.roslot-report-hero-copy p{margin:0;color:#cae3f3;font-size:11px;font-weight:650}.roslot-report-hero-date{padding-left:25px;border-left:1px solid rgba(255,255,255,.22)}.roslot-report-hero-date small{display:block;margin-bottom:7px;color:#9fc7df;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.roslot-report-hero-date strong{display:block;max-width:260px;font-size:17px;line-height:1.3}.roslot-report-hero-note{display:flex;align-items:flex-start;gap:10px;padding:13px;border:1px solid rgba(255,255,255,.17);border-radius:14px;background:rgba(255,255,255,.09);backdrop-filter:blur(8px)}.roslot-report-hero-note>i{flex:none;margin-top:2px;color:#91f0d8;font-size:19px}.roslot-report-hero-note p{margin:0}.roslot-report-hero-note strong,.roslot-report-hero-note span{display:block}.roslot-report-hero-note strong{font-size:11px}.roslot-report-hero-note span{margin-top:3px;color:#d5e8f4;font-size:9.5px;line-height:1.4}
2927.roslot-report-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:14px 0}.roslot-report-kpi{display:flex;align-items:flex-start;gap:11px;min-width:0;padding:17px;border:1px solid #dce6f0;border-radius:16px;background:#fff;box-shadow:0 8px 23px rgba(0,30,60,.055)}.roslot-report-kpi>span{display:grid;place-items:center;width:36px;height:36px;flex:0 0 36px;border-radius:11px;background:#edf5fc;color:#075ca8;font-size:17px}.roslot-report-kpi>div{min-width:0}.roslot-report-kpi small{display:block;color:#718196;font-size:8.5px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.roslot-report-kpi strong{display:block;overflow:hidden;margin-top:3px;color:#092b4c;font-size:21px;line-height:1.1;font-variant-numeric:tabular-nums;text-overflow:ellipsis}.roslot-report-kpi p{margin:4px 0 0;color:#7d8c9e;font-size:9.5px;line-height:1.35}
2928.roslot-report-ledger{padding:22px;border:1px solid #dce6f0;border-radius:20px;background:#fff;box-shadow:0 13px 34px rgba(0,30,60,.065)}.roslot-report-ledger-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px}.roslot-report-ledger-head>div:first-child>span{color:#087966;font-size:9px;font-weight:950;letter-spacing:.13em}.roslot-report-ledger-head h2{margin:4px 0 2px;font-size:21px;letter-spacing:-.02em}.roslot-report-ledger-head p{margin:0;color:#728197;font-size:10.5px}.roslot-report-search{display:grid;grid-template-columns:18px minmax(0,1fr);align-items:center;width:min(310px,100%);padding:0 11px;border:1px solid #d7e2ed;border-radius:11px;background:#f7f9fc;color:#76899d}.roslot-report-search input{min-width:0;height:39px;border:0;outline:0;background:transparent;color:#173958;font:650 11px 'Archivo',sans-serif}.roslot-report-filters{display:flex;gap:6px;margin-top:15px;padding-top:14px;border-top:1px solid #edf1f5}.roslot-report-filter{padding:7px 11px;border:1px solid #d9e4ed;border-radius:999px;background:#fff;color:#526579;font:800 10px 'Archivo',sans-serif;cursor:pointer}.roslot-report-filter.is-active{border-color:#075ca8;background:#075ca8;color:#fff}.roslot-report-transactions{display:grid;gap:12px;margin-top:16px}.roslot-report-transaction{overflow:hidden;border:1px solid #dce5ee;border-radius:17px;background:#fff;transition:transform .16s,box-shadow .16s,border-color .16s}.roslot-report-transaction:hover{transform:translateY(-1px);border-color:#bad0e4;box-shadow:0 12px 26px rgba(0,38,75,.08)}.roslot-report-transaction>header{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:16px 17px}.roslot-report-lot-id{display:flex;align-items:center;gap:13px;min-width:0}.roslot-report-lot-id>span{flex:none;padding:6px 8px;border-radius:8px;background:#092f57;color:#fff;font-size:9px;font-weight:950;letter-spacing:.05em}.roslot-report-lot-id>div{min-width:0}.roslot-report-lot-id h3{margin:0;color:#112033;font-size:15px}.roslot-report-lot-id p{overflow:hidden;margin:3px 0 0;color:#718196;font-size:9.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-transaction-total{flex:none;text-align:right}.roslot-report-transaction-total small,.roslot-report-price-strip small{display:block;
2928color:#77889a;font-size:8px;font-weight:900;letter-spacing:.08em}.roslot-report-transaction-total strong{display:block;margin-top:2px;color:#087966;font-size:22px;line-height:1;font-variant-numeric:tabular-nums}.roslot-report-transaction-total span{display:block;margin-top:5px;color:#64758a;font-size:9px;font-weight:750}.roslot-report-price-strip{display:grid;grid-template-columns:1fr 1.25fr .65fr;gap:1px;background:#e3eaf1;border-top:1px solid #e3eaf1;border-bottom:1px solid #e3eaf1}.roslot-report-price-strip>div{min-width:0;padding:11px 16px;background:#f7f9fc}.roslot-report-price-strip strong{display:block;overflow:hidden;margin-top:3px;color:#173958;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-price-strip strong>em{color:#718196;font-size:9px;font-style:normal}.roslot-report-contacts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:14px 16px}.roslot-report-contact{min-width:0;padding:13px;border:1px solid #dce5ed;border-radius:13px;background:#fbfcfe}.roslot-report-contact.is-buyer{border-left:3px solid #0a75bc}.roslot-report-contact.is-seller{border-left:3px solid #168064}.roslot-report-contact-title{display:flex;align-items:center;gap:10px}.roslot-report-contact-title>span{display:grid;place-items:center;width:34px;height:34px;flex:0 0 34px;border-radius:50%;background:#e5f1fb;color:#075ca8;font-size:10px;font-weight:950}.roslot-report-contact.is-seller .roslot-report-contact-title>span{background:#e3f4ee;color:#13745d}.roslot-report-contact-title div{min-width:0}.roslot-report-contact-title small{display:block;color:#718196;font-size:8px;font-weight:900;letter-spacing:.09em;text-transform:uppercase}.roslot-report-contact-title strong{display:block;overflow:hidden;margin-top:2px;color:#112033;font-size:11.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-contact-links{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:10px}.roslot-report-contact-links a,.roslot-report-contact-links>span{display:flex;align-items:center;gap:6px;min-width:0;padding:7px 8px;border-radius:8px;background:#f0f4f8;color:#38536d;font-size:9px;font-weight:700;text-decoration:none}.roslot-report-contact-links a:hover{background:#e2eef8;color:#075ca8}.roslot-report-contact-links i{flex:none}.roslot-report-contact-links a,.roslot-report-contact-links>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.roslot-report-contact-links>span{color:#8a98a7;font-weight:600}.roslot-report-transaction>footer{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-top:1px solid #edf1f5;background:#fbfcfe}.roslot-report-transaction>footer>span{display:inline-flex;align-items:center;gap:6px;color:#168064;font-size:9px;font-weight:900}.roslot-report-transaction>footer button{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;color:#075ca8;font:900 10px 'Archivo',sans-serif;cursor:pointer}.roslot-report-empty{padding:46px 20px;text-align:center}.roslot-report-empty>span{display:grid;place-items:center;width:46px;height:46px;margin:0 auto 11px;border-radius:15px;background:#edf5fc;color:#075ca8;font-size:21px}.roslot-report-empty h3{margin:0;color:#173958;font-size:16px}.roslot-report-empty p{margin:5px 0 0;color:#718196;font-size:11px}.roslot-report-error{display:grid;grid-template-columns:46px minmax(0,1fr) auto;gap:13px;align-items:center;padding:18px;border:1px solid #f0c5c9;border-radius:17px;background:#fff5f6}.roslot-report-error>span{display:grid;place-items:center;width:46px;height:46px;border-radius:14px;background:#fee4e7;color:#c92b3a;font-size:20px}.roslot-report-error strong{color:#932130;font-size:14px}.roslot-report-error p{margin:3px 0 0;color:#8c5860;font-size:10.5px}.roslot-report-error button{padding:9px 13px;border:0;border-radius:9px;background:#c52b3a;color:#fff;font:800 10px 'Archivo',sans-serif;cursor:pointer}.roslot-report-loading{display:grid;gap:14px}.roslot-report-loading-hero,.roslot-report-loading-grid>span,.roslot-report-loading-ledger{position:relative;overflow:hidden;background:#e7edf3}.roslot-report-loading-hero:after,.roslot-report-loading-grid>span:after,.roslot-report-loading-ledger:after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.72),transparent);animation:roslotReportShimmer 1.25s infinite}.roslot-report-loading-hero{height:150px;border-radius:24px}.roslot-report-loading-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.roslot-report-loading-grid>span{height:92px;border-radius:16px}.roslot-report-loading-ledger{height:260px;border-radius:20px}@keyframes roslotReportShimmer{to{transform:translateX(100%)}}
2929@media(max-width:930px){.roslot-report-hero{grid-template-columns:minmax(0,1fr) minmax(230px,.75fr)}.roslot-report-hero-note{grid-column:1/-1}.roslot-report-kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.roslot-report-contact-links{grid-template-columns:minmax(0,1fr)}}
2930@media(max-width:680px){.roslot-report-page{width:min(100% - 22px,1240px);padding-top:16px}.roslot-report-header{align-items:stretch;flex-direction:column;gap:14px}.roslot-report-header h1{font-size:29px}.roslot-report-back{margin-bottom:11px}.roslot-report-date-control{min-width:0}
2930.roslot-report-date-control button span{display:none}.roslot-report-hero{grid-template-columns:minmax(0,1fr);gap:18px;padding:22px;border-radius:19px}.roslot-report-hero-date{padding:15px 0 0;border-top:1px solid rgba(255,255,255,.2);border-left:0}.roslot-report-hero-note{grid-column:auto}.roslot-report-kpis,.roslot-report-loading-grid{grid-template-columns:minmax(0,1fr)}.roslot-report-ledger{padding:14px;border-radius:17px}.roslot-report-ledger-head{align-items:stretch;flex-direction:column}.roslot-report-search{width:auto}.roslot-report-transaction>header{align-items:flex-start;flex-direction:column}.roslot-report-transaction-total{text-align:left}.roslot-report-price-strip{grid-template-columns:minmax(0,1fr)}.roslot-report-contacts{grid-template-columns:minmax(0,1fr);padding:11px}.roslot-report-contact-links{grid-template-columns:minmax(0,1fr)}.roslot-report-error{grid-template-columns:38px minmax(0,1fr)}.roslot-report-error>button{grid-column:1/-1}.roslot-report-entry{padding:9px 12px}}
2931.roslot-report-row{display:grid;grid-template-columns:86px minmax(138px,1.05fr) minmax(150px,.9fr) repeat(2,minmax(158px,1fr)) auto;gap:11px;align-items:center;min-width:0;padding:10px;border:1px solid #dce5ee;border-radius:15px;background:#fff;transition:transform .16s,border-color .16s,box-shadow .16s}.roslot-report-row:hover{transform:translateY(-1px);border-color:#b8cee2;box-shadow:0 10px 22px rgba(0,38,75,.075)}.roslot-report-row-thumb{position:relative;overflow:hidden;width:86px;height:74px;border-radius:11px;background:linear-gradient(135deg,#dceaf4,#b9d0df);color:#35627f}.roslot-report-row-thumb img{width:100%;height:100%;display:block;object-fit:cover}.roslot-report-row-thumb>i{position:absolute;inset:0;margin:auto;width:23px;height:23px}.roslot-report-row-thumb>span{position:absolute;top:5px;left:5px;padding:3px 5px;border-radius:999px;background:rgba(19,116,93,.92);color:#fff;font-size:6.5px;font-weight:950;letter-spacing:.06em}.roslot-report-row-lot,.roslot-report-row-money,.roslot-report-row-contact{min-width:0}.roslot-report-row-lot>small,.roslot-report-row-money>small,.roslot-report-row-contact header small{display:block;color:#718196;font-size:7.5px;font-weight:950;letter-spacing:.09em}.roslot-report-row-lot>small{color:#075ca8}.roslot-report-row-lot h3{overflow:hidden;margin:3px 0 0;color:#112033;font-size:13px;line-height:1.15;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-lot>p{overflow:hidden;margin:3px 0 0;color:#7a899b;font-size:8px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-lot>div{display:flex;gap:7px;margin-top:7px;color:#4d647a;font-size:8px;font-weight:800}.roslot-report-row-lot>div span+span{overflow:hidden;padding-left:7px;border-left:1px solid #d9e3ec;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-money{padding-left:11px;border-left:1px solid #e3eaf0}.roslot-report-row-money>strong{display:block;overflow:hidden;margin-top:3px;color:#087966;font-size:17px;line-height:1.05;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-money>p{margin:4px 0 0;color:#173958;font-size:9px;font-weight:900}.roslot-report-row-money>p em{color:#718196;font-size:7.5px;font-style:normal}.roslot-report-row-money>span{display:block;overflow:hidden;margin-top:4px;color:#7a899b;font-size:7.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-contact{align-self:stretch;display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-rows:auto auto;column-gap:8px;align-content:center;padding:8px 9px;border:1px solid #dfe7ee;border-left:3px solid #0a75bc;border-radius:11px;background:#fafcfe}.roslot-report-row-contact.is-seller{border-left-color:#168064}.roslot-report-row-contact header{grid-column:1;display:flex;align-items:center;gap:7px;min-width:0}.roslot-report-row-contact header>span{display:grid;place-items:center;width:26px;height:26px;flex:0 0 26px;border-radius:50%;background:#e5f1fb;color:#075ca8;font-size:8px;font-weight:950}.roslot-report-row-contact.is-seller header>span{background:#e3f4ee;color:#13745d}.roslot-report-row-contact header>div{min-width:0}.roslot-report-row-contact header strong{display:block;overflow:hidden;margin-top:1px;color:#112033;font-size:9.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-contact-data{grid-column:1;display:grid;gap:2px;min-width:0;margin-top:5px}.roslot-report-row-contact-data span{overflow:hidden;color:#66798c;font-size:7.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-contact-actions{grid-column:2;grid-row:1/-1;display:grid;gap:5px;align-content:center}.roslot-report-row-contact-actions a{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-width:52px;padding:5px 6px;border:1px solid #c9dcea;border-radius:7px;background:#fff;color:#075ca8;font-size:7.5px;font-weight:900;text-decoration:none;transition:background .15s,color .15s,border-color .15s}.roslot-report-row-contact-actions a:hover{border-color:#075ca8;background:#075ca8;color:#fff}.roslot-report-row-open{display:grid;justify-items:center;gap:4px;min-width:60px;padding:9px 7px;border:1px solid #d5e2ec;border-radius:9px;background:#f6f9fc;color:#075ca8;font:900 8px 'Archivo',sans-serif;cursor:pointer}.roslot-report-row-open:hover{border-color:#075ca8;background:#075ca8;color:#fff}.roslot-report-row-open i{font-size:13px}
2932@media(max-width:1080px){.roslot-report-row{grid-template-columns:76px minmax(0,1fr) minmax(155px,.7fr) auto}.roslot-report-row-thumb{width:76px;height:70px}.roslot-report-row-contact{grid-row:2}.roslot-report-row-contact.is-buyer{grid-column:2}.roslot-report-row-contact.is-seller{grid-column:3}.roslot-report-row-open{grid-column:4;grid-row:1/3}.roslot-report-row-lot{grid-column:2}.roslot-report-row-money{grid-column:3}}
2933@media(max-width:680px){.roslot-report-row{grid-template-columns:64px minmax(0,1fr);gap:9px;padding:9px}.roslot-report-row-thumb{grid-column:1;width:64px;
2933height:62px}.roslot-report-row-lot{grid-column:2}.roslot-report-row-money{grid-column:1/-1;padding:8px 0 0;border-top:1px solid #e3eaf0;border-left:0}.roslot-report-row-contact,.roslot-report-row-contact.is-buyer,.roslot-report-row-contact.is-seller{grid-column:1/-1;grid-row:auto}.roslot-report-row-open{grid-column:1/-1;grid-row:auto;display:flex;justify-content:center;min-height:34px}.roslot-report-row-contact-actions{display:flex}.roslot-report-row-contact-actions a{min-width:60px}}
2934@media print{.app-light-topbar,.app-dark-topbar,.app-mobile-bottom-nav,.roslot-report-back,.roslot-report-date-control button,.roslot-report-search,.roslot-report-filters,.roslot-report-transaction>footer{display:none!important}.roslot-report-page{width:100%;padding:0}.roslot-report-header{margin-bottom:12px}.roslot-report-date-control{box-shadow:none}.roslot-report-hero,.roslot-report-kpi,.roslot-report-ledger{box-shadow:none}.roslot-report-transaction{break-inside:avoid}}
2935.roslot-results-layout{display:grid;grid-template-columns:230px minmax(0,1fr);gap:18px;align-items:start}.roslot-results-main{min-width:0}.roslot-filter-sidebar{position:sticky;top:128px;display:flex;flex-direction:column;gap:16px;max-height:calc(100vh - 148px);overflow-y:auto;overscroll-behavior:contain;padding:16px;border:1px solid #dce6f0;border-radius:16px;background:#fff;box-shadow:0 10px 28px rgba(0,30,60,.07);scrollbar-width:thin}.roslot-filter-sidebar-head{position:sticky;z-index:2;top:-16px;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:-16px -16px 0;padding:15px 16px 12px;border-bottom:1px solid #edf1f5;background:#fff}.roslot-filter-sidebar-head strong{color:#112033;font-size:15px}.roslot-filter-sidebar-head span{padding:3px 7px;border-radius:999px;background:#eef4fb;color:#075ca8;font-size:10px;font-weight:900}.roslot-filter-trigger{display:none!important}
2936.roslot-view-toggle{display:flex;flex:none;gap:2px;padding:4px;border:1px solid #e3eaf2;border-radius:12px;background:#fff;box-shadow:0 4px 14px rgba(0,30,60,.04)}.roslot-view-toggle span{width:38px;height:34px;display:grid;place-items:center;border-radius:9px;cursor:pointer;font-size:15px;transition:background .16s,color .16s,transform .16s}.roslot-view-toggle span:hover{transform:translateY(-1px)}
2937.roslot-lot-results-grid.is-grid{grid-template-columns:repeat(3,minmax(0,1fr))!important}
2938.roslot-lot-results-grid.is-list{grid-template-columns:minmax(0,1fr)!important}.roslot-lot-results-grid.is-list .roslot-lot-card{display:grid!important;grid-template-columns:minmax(190px,25%) minmax(0,1fr);min-height:90px}.roslot-lot-results-grid.is-list .roslot-lot-card-media{height:auto!important;min-height:90px}.roslot-lot-results-grid.is-list .roslot-lot-card-body{display:grid!important;grid-template-columns:minmax(150px,.75fr) minmax(200px,1.25fr) 104px;grid-template-rows:1fr auto;gap:4px 12px!important;align-content:center;padding:7px 10px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats{grid-column:1;grid-row:1;gap:5px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats>div{padding:5px 8px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:first-child{font-size:8.5px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:last-child{font-size:13px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-details{grid-column:2;grid-row:1/span 2;gap:2px!important;font-size:10.5px!important;justify-content:center}.roslot-lot-results-grid.is-list .roslot-lot-card-china{grid-column:1;grid-row:2;padding:2px 7px!important;font-size:9px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-actions{grid-column:3;grid-row:1/span 2;display:grid!important;align-content:center;margin-top:0!important;padding-top:0!important}.roslot-lot-results-grid.is-list .roslot-lot-card-actions button{padding:8px 6px!important;font-size:11px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>span{top:6px!important;padding:3px 7px!important;font-size:8px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:6px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:first-child{font-size:12px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:last-child{font-size:9px!important}.roslot-lot-results-grid.is-list .roslot-price-badge{padding:3px 6px!important}.roslot-lot-results-grid.is-list .roslot-price-badge>div:first-child{font-size:6.5px!important}.roslot-lot-results-grid.is-list .roslot-price-badge>div:last-child{font-size:12px!important}
2939@media(max-width:1050px){.roslot-lot-results-grid.is-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}
2940@media(max-width:760px){.roslot-ops-shell.is-mobile-open{align-content:start}.roslot-results-layout{display:block}.roslot-filter-sidebar{display:none;position:relative;top:auto;max-height:48vh;margin-top:12px}.roslot-filter-sidebar.is-mobile-open{display:flex}.roslot-filter-trigger{display:inline-flex!important}}
2941@media(max-width:760px){.roslot-view-toggle{margin-left:auto}.roslot-lot-results-grid.is-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:10px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card{border-radius:14px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-media{height:142px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-body{padding:10px!important;gap:7px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats{gap:5px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats>div{padding:6px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats>div>div:first-child{font-size:8px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats>div>div:last-child{font-size:13px!important}
2941.roslot-lot-results-grid.is-grid .roslot-lot-card-details{font-size:10.5px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-actions button{padding:9px 5px!important;font-size:11px!important}.roslot-lot-results-grid.is-list .roslot-lot-card{grid-template-columns:128px minmax(0,1fr);min-height:176px}.roslot-lot-results-grid.is-list .roslot-lot-card-media{min-height:176px}.roslot-lot-results-grid.is-list .roslot-lot-card-body{grid-template-columns:minmax(0,1fr);gap:8px!important;padding:11px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats{grid-column:1}.roslot-lot-results-grid.is-list .roslot-lot-card-details,.roslot-lot-results-grid.is-list .roslot-lot-card-china{display:none!important}.roslot-lot-results-grid.is-list .roslot-lot-card-actions{grid-column:1}.roslot-lot-results-grid.is-list .roslot-lot-card-actions button{padding:9px!important;font-size:12px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>span:first-of-type{font-size:9px!important;padding:4px 7px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:8px!important;display:grid!important}.roslot-lot-results-grid.is-list .roslot-price-badge{justify-self:start;text-align:left!important;margin-top:5px}.roslot-lot-results-grid.is-list .roslot-price-badge>div{justify-content:flex-start!important}}
2942@media(max-width:760px){
2943  body:has(.roslot-pool-page) .app-mobile-load-fab{display:none!important}
2944  .roslot-mobile-dock{top:auto!important;right:12px!important;bottom:calc(78px + env(safe-area-inset-bottom))!important;width:42px;height:42px;justify-content:center!important;padding:0!important;border-radius:50%!important;transform:none!important;font-size:0!important;letter-spacing:0!important;writing-mode:horizontal-tb!important}
2945  .roslot-mobile-dock img{width:18px!important;height:18px!important;transform:none!important}
2946  .roslot-search-toolbar{display:grid!important;grid-template-columns:42px minmax(0,1fr) 78px;gap:8px!important}
2947  .roslot-search-box{grid-column:1/-1;min-width:0!important}
2948  .roslot-search-toolbar>.roslot-filter-trigger{width:42px;min-width:42px;justify-content:center;padding:11px 0!important;gap:0!important;font-size:0!important}
2949  .roslot-search-toolbar>select{width:100%;min-width:0;padding-left:10px!important;padding-right:6px!important}
2950  .roslot-search-toolbar>.roslot-view-toggle{width:78px;box-sizing:border-box;margin:0!important;padding:4px 3px!important;justify-content:space-between}
2951  .roslot-search-toolbar>.roslot-view-toggle span{width:33px;height:32px}
2952  .roslot-lot-results-grid.is-grid .roslot-lot-card-media{height:168px!important}
2953  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>span{top:7px!important;padding:3px 6px!important;font-size:7.5px!important}
2954  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>span:first-of-type{left:7px!important}
2955  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>span:nth-of-type(2){right:7px!important;left:auto!important;gap:3px!important}
2956  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:7px!important;display:block!important}
2957  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child>div:first-child{min-width:0;width:100%}
2958  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child>div:first-child>div:first-child{display:-webkit-box;overflow:hidden;font-size:12px!important;line-height:1.08!important;-webkit-box-orient:vertical;-webkit-line-clamp:3}
2959  .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child>div:first-child>div:last-child{margin-top:2px;font-size:9px!important}
2960  .roslot-lot-results-grid.is-grid .roslot-price-badge{display:inline-block!important;margin-top:5px;padding:3px 6px!important;text-align:left!important}
2961  .roslot-lot-results-grid.is-grid .roslot-price-badge>div{justify-content:flex-start!important}
2962  .roslot-lot-results-grid.is-grid .roslot-price-badge>div:first-child{font-size:6.5px!important}
2963  .roslot-lot-results-grid.is-grid .roslot-price-badge>div:last-child{font-size:12px!important}
2964  .roslot-lot-results-grid.is-grid .roslot-lot-card-details>div:not(:nth-child(2)){display:none!important}
2965  .roslot-lot-results-grid.is-grid .roslot-lot-card-details>div:nth-child(2){font-size:10px!important}
2966  .roslot-lot-results-grid.is-grid .roslot-lot-card-china{padding:3px 7px!important;font-size:9px!important}
2967  .roslot-lot-results-grid.is-grid .roslot-lot-card-china img{width:13px!important;height:10px!important}
2968  .roslot-lot-results-grid.is-list .roslot-lot-card{grid-template-columns:122px minmax(0,1fr)!important;min-height:168px!important;border-radius:14px!important}
2969  .roslot-lot-results-grid.is-list .roslot-lot-card-media{height:168px!important;min-height:168px!important}
2970  .roslot-lot-results-grid.is-list .roslot-lot-card-media>span:first-of-type{top:7px!important;left:7px!important;padding:3px 5px!important;font-size:7px!important}
2971  .roslot-lot-results-grid.is-list .roslot-lot-card-media>span:nth-of-type(2){top:26px!important;right:7px!important;left:auto!important;padding:3px 5px!important;font-size:7px!important;gap:3px!important}
2972  .roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:7px!important;display:block!important}
2973  .roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:first-child{display:-webkit-box;overflow:hidden;font-size:11px!important;line-height:1.08!important;-webkit-box-orient:vertical;-webkit-line-clamp:3}
2974  .roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:last-child{margin-top:2px;font-size:8px!important}
2975  .roslot-lot-results-grid.is-list .roslot-price-badge{display:inline-block!important;margin-top:5px!important;padding:3px 6px!important;text-align:left!important}
2976  .roslot-lot-results-grid.is-list .roslot-price-badge>div{justify-content:flex-start!important}
2977  .roslot-lot-results-grid.is-list .roslot-price-badge>div:first-child{font-size:6px!important}
2978  .roslot-lot-results-grid.is-list .roslot-price-badge>div:last-child{font-size:11px!important}
2979  .roslot-lot-results-grid.is-list .roslot-lot-card-body{display:grid!important;grid-template-columns:minmax(0,1fr)!important;grid-template-rows:auto auto auto auto!important;align-content:center;gap:6px!important;padding:8px!important}
2980  .roslot-lot-results-grid.is-list .roslot-lot-card-stats{grid-column:1!important;grid-row:auto!important;gap:5px!important}
2981  .roslot-lot-results-grid.is-list .roslot-lot-card-stats>div{padding:5px 6px!important}
2982  .roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:first-child{font-size:7.5px!important}
2983  .roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:last-child{font-size:12px!important}
2984  .roslot-lot-results-grid.is-list .roslot-lot-card-details{display:flex!important;grid-column:1!important;grid-row:auto!important;font-size:9.5px!important}
2985  .roslot-lot-results-grid.is-list .roslot-lot-card-details>div:not(:nth-child(2)){display:none!important}
2986  .roslot-lot-results-grid.is-list .roslot-lot-card-china{display:inline-flex!important;grid-column:1!important;grid-row:auto!important;padding:2px 6px!important;font-size:8.5px!important}
2987  .roslot-lot-results-grid.is-list .roslot-lot-card-china img{width:12px!important;height:9px!important}
2988  .roslot-lot-results-grid.is-list .roslot-lot-card-actions{display:flex!important;grid-column:1!important;grid-row:auto!important;margin:0!important;padding:0!important}
2989  .roslot-lot-results-grid.is-list .roslot-lot-card-actions button{padding:7px 5px!important;font-size:10.5px!important}
2990}
2991.tv-video-tile{position:relative;min-width:0;overflow:hidden;border-radius:18px;background:#031426;border:1px solid rgba(255,255,255,.24);box-shadow:0 24px 54px rgba(0,0,0,.3)}
2992.tv-video-layer.is-single .tv-video-tile{position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:0;box-shadow:none;background:transparent;opacity:0;transition:opacity .28s ease}
2993.tv-video-layer.is-single .tv-video-tile.is-active{opacity:1}
2994.tv-video-layer.is-waiting-background .tv-video-tile{transition:opacity 1.1s ease}
2995.tv-video-layer.is-waiting-background .tv-video-tile video{filter:saturate(.58) contrast(.88);transform:scale(1.035)}
2996.tv-video-layer.is-waiting-background .tv-video-badge,.tv-video-layer.is-waiting-background .tv-video-sale-badge{display:none}
2997.tv-video-layer.is-grid .tv-video-tile{aspect-ratio:16/9;width:100%}
2998.tv-video-layer.is-grid.is-count-4 .tv-video-tile{height:100%;aspect-ratio:auto}
2999.tv-video-tile video{display:block;width:100%;height:100%;object-fit:cover;background:transparent;opacity:0;transition:opacity .18s ease}
3000.tv-video-tile video.is-ready{opacity:1}
3001.tv-video-tile video{filter:grayscale(0);transition:opacity .18s ease,filter .72s cubic-bezier(.2,.72,.24,1)}
3002.tv-video-tile.is-sold video{filter:grayscale(1)}
3003.tv-video-badge{position:absolute;
3003left:14px;top:14px;z-index:2;display:inline-flex;align-items:center;min-height:32px;padding:0 13px;border-radius:999px;background:rgba(0,46,95,.92);border:1px solid rgba(255,255,255,.24);color:#fff;font-size:clamp(13px,1vw,18px);font-weight:950;letter-spacing:.04em;text-transform:uppercase;box-shadow:0 12px 28px rgba(0,0,0,.24)}
3004.tv-video-sale-badge{position:absolute;left:50%;bottom:clamp(14px,1.5vw,24px);z-index:2;transform:translate(-50%,12px) scale(.92);display:inline-flex;align-items:center;justify-content:center;gap:clamp(10px,.8vw,15px);min-width:min(72%,360px);min-height:clamp(48px,4.2vw,68px);box-sizing:border-box;padding:8px clamp(20px,2vw,34px) 8px 10px;border-radius:999px;background:rgba(0,145,96,.97);border:2px solid rgba(255,255,255,.78);color:#fff;font-size:clamp(16px,1.35vw,24px);font-weight:950;letter-spacing:.07em;text-align:center;text-transform:uppercase;white-space:nowrap;opacity:0;box-shadow:0 16px 38px rgba(0,0,0,.38),0 0 24px rgba(0,145,96,.3);transition:opacity .36s ease,transform .48s cubic-bezier(.2,.82,.24,1)}.tv-video-sale-badge.is-visible{opacity:1;transform:translate(-50%,0) scale(1)}.tv-video-sale-badge i{width:clamp(30px,2.5vw,40px);height:clamp(30px,2.5vw,40px);display:grid;place-items:center;flex:0 0 auto;border-radius:50%;background:#fff;color:#00875a;font-size:clamp(20px,1.7vw,28px);font-style:normal;font-weight:950;line-height:1;box-shadow:0 5px 14px rgba(0,73,49,.24)}
3005.tv-video-layer.is-single .tv-video-badge{left:3vw;top:auto;bottom:max(29.1vh,calc(4.4vh + 196px))}
3006.tv-video-layer.is-single.is-without-lower-third .tv-video-badge{bottom:3vh}
3007@media(max-height:760px){.tv-lower-third{bottom:3vh;height:clamp(128px,25vh,190px);max-height:calc(100vh - 6vh);grid-template-columns:clamp(138px,13vw,220px) minmax(0,1fr);grid-template-rows:clamp(30px,5vh,40px) minmax(0,1fr)}.tv-lower-topline{gap:10px;padding-left:14px;padding-right:14px}.tv-lower-title-line{gap:8px}.tv-lower-title-line:before{width:7px;height:7px;flex-basis:7px}.tv-lower-meta{gap:6px}.tv-lower-lot-panel{padding:7px 11px}.tv-lower-logo{height:30px}.tv-lower-logo img{max-width:112px;max-height:28px}.tv-lower-logo strong{font-size:15px}.tv-lower-lot{gap:3px;font-size:clamp(20px,2.5vw,34px)}.tv-lower-lot small{font-size:clamp(10px,1.05vw,15px)}.tv-lower-table{grid-template-rows:clamp(23px,4vh,31px)}.tv-lower-row>*{padding-left:7px;padding-right:7px;font-size:clamp(12px,1.26vw,20px)}.tv-lower-head>*{font-size:clamp(10px,1vw,15px)}.tv-lower-table.is-many .tv-lower-row>*{font-size:clamp(11px,1.02vw,16px);padding-left:5px;padding-right:5px}.tv-lower-table.is-max .tv-lower-row>*{font-size:clamp(10px,.82vw,14px);padding-left:4px;padding-right:4px}.tv-channel-bug{top:3vh;gap:6px;width:min(58vw,620px)}.tv-bug-strip{height:clamp(40px,7vh,54px)}.tv-bug-part{gap:7px;padding-left:11px;padding-right:11px}.tv-bug-rosgan{padding-left:14px}.tv-bug-logo{max-height:34px}.tv-bug-cons .tv-bug-logo{width:31px;height:31px;flex-basis:31px}.tv-bug-live{min-height:34px;gap:7px}.tv-bug-clock,.tv-bug-live-card{min-height:32px}.tv-bug-clock{padding-left:12px;padding-right:11px;font-size:15px}.tv-bug-clock:before{width:14px;height:14px;flex-basis:14px}.tv-bug-live-card{padding-left:12px;padding-right:12px;gap:7px}.tv-bug-live-card b{font-size:12px}.tv-bug-live-card span{font-size:14px}.tv-weather-bug{top:3vh;min-height:clamp(40px,7vh,54px);gap:8px;padding-left:12px;padding-right:14px}.tv-weather-icon{width:23px;height:23px;flex-basis:23px}.tv-weather-temp{font-size:22px}.tv-weather-copy strong,.tv-weather-copy small{font-size:10px}.tv-video-layer.is-grid{top:max(16vh,calc(3vh + 104px));bottom:max(28vh,calc(3vh + 164px))}.tv-video-layer.is-single .tv-video-badge{bottom:max(28vh,calc(3vh + 164px))}}
3008
3009.tv-channel-bug .tv-bug-strip{width:100%;box-sizing:border-box}
3010.tv-bug-cabana{background:var(--tv-cabana-logo-background,#fff);color:#123858}
3011.tv-bug-cabana .tv-bug-logo{width:100%;height:44px;max-width:190px;object-fit:contain}
3012.tv-bug-cabana strong{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.15;text-align:center;font-size:clamp(12px,1.15vw,17px)}
3013.tv-channel-bug .tv-bug-live{width:100%;align-items:flex-start}
3014.tv-bug-live .tv-bug-clock{flex:0 0 auto;white-space:nowrap}
3015.tv-bug-live .tv-bug-live-card{flex:1;max-width:none;padding-top:10px;padding-bottom:10px;border-radius:22px;box-sizing:border-box}
3016.tv-bug-live-card span{white-space:normal;overflow:visible;text-overflow:clip;overflow-wrap:anywhere;line-height:1.22;font-size:clamp(14px,1.3vw,20px)}
3017@media(max-width:1100px){.tv-channel-bug{left:2vw;width:min(68vw,620px)}.tv-weather-bug{right:2vw}.tv-lower-third{left:2vw;right:2vw;grid-template-columns:clamp(132px,16vw,190px) minmax(0,1fr)}.tv-lower-topline{padding-left:12px;padding-right:12px}.tv-lower-meta{display:none}.tv-lower-row>*{padding-left:5px;padding-right:5px}}
3018.tv-config-screen-elements{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}
3019.tv-config-contacts{display:grid;gap:8px;margin-top:18px;font-weight:700}.tv-config-contacts textarea{width:100%;box-sizing:border-box;min-height:85px;padding:12px;border:1px solid #c8daec;border-radius:10px;font:inherit;resize:vertical}.tv-config-contacts small{font-weight:400;color:#54718c}
3020.tv-phone-ticker{position:absolute;left:2.6vw;right:2.6vw;bottom:.6vh;height:3.2vh;display:flex;align-items:center;gap:1.5vw;padding:0 1vw;box-sizing:border-box;background:#f3f7f8;color:#123d5b;border-radius:4px;z-index:4;font-size:2.2vh;font-weight:800;overflow:hidden}.tv-phone-ticker>strong{flex:none;font-size:1.3vh;letter-spacing:.08em}.tv-phone-ticker-window{flex:1;min-width:0;overflow:hidden}.tv-phone-ticker-track{display:flex;width:max-content;animation:tvPhoneTicker 35s linear infinite}.tv-phone-ticker-track span{white-space:nowrap;padding-right:5vw}@keyframes tvPhoneTicker{to{transform:translateX(-50%)}}
3021.tv-config-side{display:grid;gap:12px;align-content:start}
3022.tv-config-screen-toggle{height:42px;border:1px solid #dbe7f2;border-radius:11px;background:#fff;color:#173a5d;font:900 12px 'Archivo',sans-serif;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 10px 0 12px;cursor:pointer}
3023.tv-config-screen-toggle i{width:42px;height:24px;border-radius:999px;background:#94a3b8;padding:3px;box-sizing:border-box;transition:background .18s;flex:0 0 42px}
3024.tv-config-screen-toggle b{display:block;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 3px 8px rgba(0,0,0,.16);transition:transform .18s}
3025.tv-config-screen-toggle.is-on{border-color:#bfe6d5;background:#f0fbf6;color:#087244}
3026.tv-config-screen-toggle.is-on i{background:#12b981}
3027.tv-config-screen-toggle.is-on b{transform:translateX(18px)}
3028.tv-config-screen-toggle:disabled{cursor:not-allowed;opacity:.48;filter:saturate(.45)}
3029.tv-config-lot{grid-template-columns:54px minmax(0,1fr) 42px 24px}
3030.tv-config-lot-status{width:22px;height:22px;display:grid;place-items:center;border-radius:50%;font-size:14px;font-style:normal;font-weight:950;line-height:1}
3031.tv-config-lot-status.is-complete{background:#12b981;color:#fff;box-shadow:0 5px 12px rgba(18,185,129,.25)}
3032.tv-config-lot-status.is-empty{visibility:hidden}
3033.tv-config-lot.is-all-lifted{border-color:#d5dde6;background:#edf1f5;color:#8995a3;box-shadow:none;cursor:not-allowed;filter:grayscale(1)}
3034.tv-config-lot.is-all-lifted strong,.tv-config-lot.is-all-lifted span,.tv-config-lot.is-all-lifted small{color:#8995a3}
3035.tv-config-lot.is-all-lifted .tv-config-lot-status{background:#a5afba;color:#fff}
3036.tv-config-lot:disabled{opacity:.72}
3037.tv-config-video-mode{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}
3038.tv-config-video-mode button{height:34px;border:1px solid #d2e1ef;border-radius:999px;background:#fff;color:#173a5d;padding:0 13px;font:900 12px 'Archivo',sans-serif;cursor:pointer}
3039.tv-config-video-mode button.is-active{background:#002e5f;border-color:#002e5f;color:#fff}
3040.tv-config-transport{display:grid;gap:10px;margin-top:12px;padding:12px;border:1px solid #cfe0f1;border-radius:12px;background:linear-gradient(145deg,#fff,#f1f7fd)}
3041.tv-config-transport-head{display:flex;align-items:end;justify-content:space-between;gap:12px}.tv-config-transport-head span{display:grid;gap:2px;min-width:0}.tv-config-transport-head small{color:#71839a;font-size:9px;font-weight:950;letter-spacing:.11em;text-transform:uppercase}.tv-config-transport-head strong{overflow:hidden;color:#002e5f;font-size:15px;text-overflow:ellipsis;white-space:nowrap}.tv-config-transport-head em{color:#087244;font-size:11px;font-style:normal;font-weight:950;white-space:nowrap}
3042.tv-config-transport select{width:100%;height:38px;border:1px solid #cfe0f1;border-radius:10px;background:#fff;color:#173a5d;padding:0 10px;font:900 12px 'Archivo',sans-serif;outline:0}
3043.tv-config-transport-controls{display:grid;grid-template-columns:1fr auto auto 1fr;gap:7px}.tv-config-transport-controls button{min-height:38px;border:1px solid #cfe0f1;border-radius:10px;background:#fff;color:#173a5d;padding:0 10px;font:900 11px 'Archivo',sans-serif;cursor:pointer}.tv-config-transport-controls button.is-primary{min-width:94px;border-color:#002e5f;background:#002e5f;color:#fff}.tv-config-transport-controls button:disabled,.tv-config-transport select:disabled{cursor:not-allowed;opacity:.48}
3044@media(max-width:520px){.tv-config-transport-controls{grid-template-columns:1fr 1fr}.tv-config-transport-controls button.is-primary{min-width:0}}
3045@media(max-width:360px){.dc-lot-contact-actions{grid-template-columns:1fr}}
3046
3047/* Navegación y formularios móviles: una sola geometría estable en todas las rutas. */
3048.app-light-topbar,
3049.app-mobile-bottom-nav {
3050  width:100%;
3051  max-width:100vw;
3052  min-width:0;
3053  box-sizing:border-box;
3054}
3055.app-mobile-bottom-nav {
3056  isolation:isolate;
3057  overscroll-behavior:contain;
3058}
3059.app-mobile-bottom-nav > span {
3060  min-width:0;
3061  max-width:20%;
3062}
3063.feria-creator-layout,
3064.feria-form-card,
3065.feria-preview-card,
3066.feria-field-grid,
3067.feria-consignataria-grid,
3068.feria-form-card label {
3069  min-width:0;
3070  max-width:100%;
3071}
3072.feria-form-card input,
3073.feria-form-card select,
3074.feria-form-card textarea {
3075  width:100%;
3076  min-width:0;
3077}
3078@media(max-width:760px){
3079  .app-light-topbar {
3080    left:0!important;
3081    right:0!important;
3082    padding:10px max(14px,env(safe-area-inset-right)) 10px max(14px,env(safe-area-inset-left))!important;
3083  }
3084  .app-mobile-bottom-nav {
3085    left:0!important;
3086    right:0!important;
3087    bottom:0!important;
3088    min-height:68px;
3089    padding:8px max(6px,env(safe-area-inset-right)) max(10px,env(safe-area-inset-bottom)) max(6px,env(safe-area-inset-left))!important;
3090    border-radius:0!important;
3091    transform:none!important;
3092    box-shadow:0 -8px 28px rgba(0,29,61,.1);
3093  }
3094  .app-mobile-load-fab {
3095    right:max(12px,env(safe-area-inset-right))!important;
3096    bottom:calc(76px + env(safe-area-inset-bottom))!important;
3097    max-width:calc(100vw - 24px)!important;
3098  }
3099  .feria-creator-layout {
3100    grid-template-columns:minmax(0,1fr)!important;
3101  }
3102  .feria-form-card,
3103  .feria-preview-card,
3104  .feria-field-grid,
3105  .feria-consignataria-grid {
3106    width:100%;
3107    box-sizing:border-box;
3108  }
3109  .feria-preview-card { position:static!important; }
3110}
3111@media(max-width:360px){
3112  .feria-form-card {
3113    padding:18px!important;
3114    border-radius:16px!important;
3115  }
3116  .feria-consignataria-grid {
3117    grid-template-columns:minmax(0,1fr)!important;
3118  }
3119  .app-mobile-load-fab {
3120    padding:12px 17px!important;
3121    font-size:13px!important;
3122  }
3123}
3124@supports (-webkit-touch-callout:none){
3125  .app-light-topbar,
3126  .app-mobile-bottom-nav {
3127    -webkit-backdrop-filter:none!important;
3128    backdrop-filter:none!important;
3129    background:#fff!important;
3130  }
3131}
3132</style>
3133<style id="roslot-operations-compact-layout">
3134.roslot-op-row{grid-template-columns:minmax(0,1fr) 145px auto!important;min-height:58px!important;padding:8px 10px!important}
3135.roslot-op-topline{min-height:22px!important}.roslot-op-meta-line{display:flex!important;align-items:center!important;gap:10px!important;min-width:0!important;margin-top:3px!important}.roslot-op-meta-line p{margin:0!important;min-width:0!important}.roslot-op-meta-line .roslot-op-status{flex:none!important;margin:0!important}
3136.roslot-op-alert{flex:none!important;width:auto!important;margin:0 0 0 auto!important;padding:4px 7px!important;border-radius:7px!important;box-shadow:0 4px 10px rgba(191,28,44,.2)!important}.roslot-op-alert i{font-size:12px!important}.roslot-op-alert strong{font-size:9px!important}.roslot-op-alert span{font-size:8px!important}
3137.roslot-op-price{align-self:stretch!important;display:grid!important;grid-template-columns:auto!important;align-content:center!important;justify-items:end!important;gap:2px!important;min-width:0!important;width:auto!important;margin:0!important;padding:6px 11px!important;border-radius:10px!important}.roslot-op-price>span{font-size:8px!important}.roslot-op-price strong{font-size:24px!important;line-height:.95!important}.roslot-op-price strong>.sc-interp{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}.roslot-op-price em{font-size:8px!important}
3138.roslot-op-row.is-winning .roslot-op-price,.roslot-op-row.is-seller .roslot-op-price{border-color:#bcded3!important;background:#f0faf6!important}.roslot-op-row.is-winning .roslot-op-price strong,.roslot-op-row.is-seller .roslot-op-price strong{color:#14745f!important}
3139@media(max-width:760px){.roslot-op-row{grid-template-columns:minmax(0,1fr) minmax(118px,auto)!important;gap:7px!important;min-height:64px!important}.roslot-op-topline{flex-wrap:wrap!important}.roslot-op-alert{margin-left:0!important}
3139.roslot-op-alert span{display:none!important}.roslot-op-price{min-width:108px!important}.roslot-op-price strong{font-size:22px!important}.roslot-op-actions{grid-column:1/-1!important}.roslot-op-meta-line{align-items:flex-start!important;flex-direction:column!important;gap:3px!important}}
3140</style>
3141<style id="rosgan-remate-operations-styles">
3142.rosgan-ops-toolbar{display:flex;gap:6px;flex-wrap:wrap}.rosgan-ops-toolbar button{min-height:42px;display:inline-flex;align-items:center;gap:7px;padding:0 11px;border:1px solid #cbd9e7;border-radius:11px;background:#fff;color:#173b5c;font:800 11px inherit;cursor:pointer}.rosgan-ops-page{min-height:100vh;background:#f2f6fa;color:#10233a;padding:26px 18px 90px}.rosgan-ops-shell{width:min(1180px,100%);margin:0 auto}.rosgan-ops-back{display:inline-flex;align-items:center;gap:7px;border:0;background:transparent;color:#0a5ca8;font:800 13px inherit;cursor:pointer;padding:4px 0 16px}.rosgan-ops-hero{display:flex;align-items:center;justify-content:space-between;gap:22px;padding:24px 27px;border-radius:18px;background:linear-gradient(120deg,#002e5f,#0a5ca8);color:#fff;box-shadow:0 16px 36px rgba(0,46,95,.18)}.rosgan-ops-hero small{display:block;color:#9fc5e8;font-size:10px;font-weight:900;letter-spacing:.12em}.rosgan-ops-hero h1{margin:5px 0 4px;font-size:clamp(26px,4vw,38px)}.rosgan-ops-hero p{margin:0;color:#d1e1ef;font-size:13px}.rosgan-ops-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.rosgan-ops-actions button{min-height:39px;border:1px solid rgba(255,255,255,.28);border-radius:10px;background:rgba(255,255,255,.1);color:#fff;padding:0 12px;font:800 11px inherit;cursor:pointer}.rosgan-ops-actions button.is-primary{background:#fff;color:#003f78}.rosgan-ops-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:14px 0}.rosgan-ops-metric{padding:16px 18px;border:1px solid #dbe5ef;border-radius:14px;background:#fff}.rosgan-ops-metric small{display:block;color:#7c8da2;font-size:10px;font-weight:900;letter-spacing:.07em}.rosgan-ops-metric strong{display:block;margin-top:5px;font-size:24px}.rosgan-commercial-list{display:grid;gap:8px}.rosgan-commercial-row{display:grid;grid-template-columns:72px minmax(190px,1.4fr) minmax(130px,.8fr) minmax(210px,1fr) minmax(210px,1fr);align-items:center;gap:12px;padding:13px;border:1px solid #dce6ef;border-radius:14px;background:#fff}.rosgan-commercial-id small,.rosgan-commercial-copy small,.rosgan-commercial-price small{display:block;color:#8494a8;font-size:9px;font-weight:900;letter-spacing:.07em}.rosgan-commercial-id strong{display:block;font-size:17px}.rosgan-commercial-copy strong{display:block;font-size:14px}.rosgan-commercial-copy span{display:block;margin-top:3px;color:#687b91;font-size:11px}.rosgan-ops-status{display:inline-flex;justify-content:center;padding:6px 9px;border-radius:99px;background:#eef3f7;color:#536980;font-size:10px;font-weight:900}.rosgan-ops-status.is-vendido{background:#e4f6ef;color:#08745a}.rosgan-ops-status.is-levantado{background:#fff0f1;color:#ad2c39}.rosgan-commercial-statuses,.rosgan-commercial-modes{display:flex;gap:4px}.rosgan-commercial-statuses button,.rosgan-commercial-modes button{min-height:28px;border:1px solid #d4e0eb;border-radius:8px;background:#fff;color:#526b83;padding:0 7px;font:800 9px inherit;cursor:pointer}.rosgan-commercial-modes button.is-active{border-color:#0a5ca8;background:#e8f3fc;color:#07528f}.rosgan-commercial-price input{width:100%;box-sizing:border-box;margin-top:5px;border:1px solid #ccd9e6;border-radius:9px;padding:9px 10px;font:800 13px inherit;outline:none}.rosgan-price-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin-top:16px}.rosgan-price-card{overflow:hidden;border:1px solid #dce6ef;border-radius:18px;background:#fff;box-shadow:0 8px 24px rgba(0,35,70,.07)}.rosgan-price-card>header{display:block!important;padding:17px 19px!important;background:#002e5f!important;color:#fff!important}.rosgan-price-card>header small{display:block!important;padding:0!important;background:transparent!important;color:#9fc5e8!important;font-size:9px;font-weight:900;letter-spacing:.1em}.rosgan-price-card>header strong{display:block!important;margin-top:4px;padding:0!important;background:transparent!important;color:#fff!important;font-size:20px}.rosgan-price-values{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:#e2eaf2}.rosgan-price-values>span{padding:16px 12px;background:#fff}.rosgan-price-values>span small{display:block;color:#8292a6;font-size:9px;font-weight:900}.rosgan-price-values>span strong{display:block;margin-top:5px;font-size:17px}.rosgan-ops-empty{margin-top:15px;padding:36px;border:1px dashed #c9d7e5;border-radius:16px;background:#fff;color:#6b7e92;text-align:center;font-size:13px}.rosgan-tv-config-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,.7fr);gap:14px;margin-top:14px}.rosgan-tv-config-card{padding:20px;border:1px solid #dbe5ef;border-radius:16px;background:#fff}.rosgan-tv-config-card h2{margin:0 0 15px;font-size:18px}.rosgan-tv-selector{display:grid;grid-template-columns:auto 1fr auto;gap:8px}.rosgan-tv-selector button,.rosgan-tv-selector select{min-height:42px;border:1px solid #cad8e6;border-radius:10px;background:#fff;color:#183b5a;padding:0 11px;font:800 12px inherit}.rosgan-tv-toggle-list{display:grid;gap:8px}.rosgan-tv-toggle{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;border:1px solid #d7e2ec;border-radius:11px;background:#f8fafc;color:#243f5a;padding:0 12px;font:800 12px inherit;cursor:pointer}.rosgan-tv-toggle i{position:relative;width:36px;height:20px;border-radius:99px;background:#bdcad7}.rosgan-tv-toggle i:after{content:"";position:absolute;width:14px;
3142height:14px;left:3px;top:3px;border-radius:50%;background:#fff;transition:.18s}.rosgan-tv-toggle.is-on i{background:#0a5ca8}.rosgan-tv-toggle.is-on i:after{left:19px}.rosgan-tv-contact{display:block;margin-top:15px;color:#63788e;font-size:10px;font-weight:900}.rosgan-tv-contact input{width:100%;box-sizing:border-box;margin-top:6px;border:1px solid #cad8e6;border-radius:10px;padding:11px;font:700 12px inherit}.rosgan-tv-url{display:block;margin-top:12px;padding:11px;border-radius:9px;background:#eef4f9;color:#426078;font:700 11px monospace;word-break:break-all}.rosgan-tv-output{position:fixed;inset:0;z-index:120;background:#07111d;overflow:hidden}.rosgan-tv-output video,.rosgan-tv-fallback{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.rosgan-tv-fallback{background-position:center;background-size:cover}.rosgan-tv-fallback:after{content:"";position:absolute;inset:0;background:rgba(0,20,40,.16)}.rosgan-tv-brand-bug{position:absolute;z-index:3;left:2.2vw;top:5.5vh;height:7.2vh;min-height:52px;display:flex;align-items:stretch;border-radius:4px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,.24)}.rosgan-tv-brand-bug>span{display:grid;place-items:center;padding:8px 18px;background:#003e76}.rosgan-tv-brand-bug .rosgan-tv-rosgan{width:145px;box-sizing:border-box}.rosgan-tv-brand-bug img{display:block;max-width:100%;max-height:100%;object-fit:contain}.rosgan-tv-brand-bug .rosgan-tv-cons{min-width:125px;background:var(--rosgan-cons-color);color:#fff;font-size:22px;font-weight:900}.rosgan-tv-lot-chip{position:absolute;z-index:3;left:50%;top:6vh;transform:translateX(-50%);min-width:170px;padding:16px 32px;border-radius:2px;background:#003b72;color:#fff;text-align:center;font-size:22px;font-weight:900;box-shadow:0 4px 15px rgba(0,0,0,.22)}.rosgan-tv-lower{position:absolute;z-index:3;left:2.2vw;right:2.2vw;bottom:3.8vh;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.2)}.rosgan-tv-mainline{display:grid;grid-template-columns:94px 115px minmax(210px,.7fr) minmax(300px,1.25fr);align-items:stretch;min-height:54px}.rosgan-tv-lot-block{grid-row:1/3;display:grid;place-items:center;border-radius:16px 0 0 16px;background:#113d70;text-align:center}.rosgan-tv-lot-block small{display:block;font-size:15px}.rosgan-tv-lot-block strong{display:block;font-size:28px}.rosgan-tv-cert,.rosgan-tv-category,.rosgan-tv-quantity{display:flex;align-items:center;padding:0 20px;font-size:18px;font-weight:900}.rosgan-tv-cert{background:#173f73}.rosgan-tv-category{background:#241e20}.rosgan-tv-quantity{background:#f8f8f7;color:#17191c;text-shadow:none;font-size:20px}.rosgan-tv-subline{display:grid;grid-template-columns:94px 260px 1fr;min-height:48px}.rosgan-tv-subline:before{content:""}.rosgan-tv-location,.rosgan-tv-conditions{display:flex;align-items:center;padding:0 20px;font-size:17px;font-weight:850}.rosgan-tv-location{background:#173f73}.rosgan-tv-conditions{background:#241e20}.rosgan-tv-contacts{margin-top:4px;padding:5px 12px;border-radius:0 12px 12px 0;background:#241e20;font-size:17px;font-weight:900;letter-spacing:.02em;white-space:nowrap;overflow:hidden}.rosgan-tv-no-video{position:absolute;z-index:2;inset:0;display:grid;place-items:center;color:#fff;font-size:16px;font-weight:800}.rosgan-tv-no-lot{position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-size:18px}.rosgan-tv-preview-note{margin-top:12px;color:#64798e;font-size:12px;line-height:1.45}
3143@media(max-width:900px){.rosgan-ops-metrics{grid-template-columns:repeat(2,1fr)}.rosgan-commercial-row{grid-template-columns:64px 1fr 1fr}.rosgan-commercial-row>*:nth-child(n+4){grid-column:span 1}.rosgan-tv-config-grid{grid-template-columns:1fr}.rosgan-tv-mainline{grid-template-columns:70px 90px minmax(130px,.7fr) 1.2fr}.rosgan-tv-subline{grid-template-columns:70px 190px 1fr}.rosgan-tv-cert,.rosgan-tv-category,.rosgan-tv-quantity,.rosgan-tv-location,.rosgan-tv-conditions{padding:0 10px;font-size:13px}.rosgan-tv-contacts{font-size:12px}}
3144@media(max-width:620px){.rosgan-ops-page{padding:16px 12px 86px}.rosgan-ops-hero{display:block;padding:21px}.rosgan-ops-actions{justify-content:flex-start;margin-top:16px}.rosgan-ops-metrics{gap:7px}.rosgan-commercial-row{grid-template-columns:56px 1fr}.rosgan-commercial-row>*:nth-child(n+3){grid-column:1/-1}.rosgan-price-card div{grid-template-columns:1fr}.rosgan-tv-brand-bug{transform:scale(.68);transform-origin:left top}.rosgan-tv-lot-chip{top:4vh;min-width:100px;padding:10px 16px;font-size:14px}.rosgan-tv-lower{left:10px;right:10px}.rosgan-tv-mainline{grid-template-columns:58px 72px 1fr}.rosgan-tv-quantity{grid-column:2/-1;min-height:38px}.rosgan-tv-lot-block{grid-row:1/3}.rosgan-tv-subline{grid-template-columns:58px 1fr}.rosgan-tv-conditions{grid-column:2/-1;min-height:38px}.rosgan-tv-contacts{font-size:9px}}
3145  .rosgan-tv-brand-bug .rosgan-tv-rosgan{background:#fff}
3146/* Broadcast identity: viewport-scaled type, explicit logo boxes and a single aligned lower third. */
3147html:has(.rosgan-tv-output),body:has(.rosgan-tv-output){overflow:hidden!important}
3148.rtv-brand{position:absolute;z-index:3;left:4.5vw;top:5vh;display:flex;align-items:center;height:4.6vw;min-height:38px;max-height:88px;filter:drop-shadow(0 6px 16px #00172a30)}
3149.rtv-brand-rosgan{display:flex;align-items:center;justify-content:center;width:10vw;height:100%;padding:1vw;background:#fff;border-radius:.55vw 0 0 .55vw;box-sizing:border-box}
3150.rtv-brand-rosgan img{width:100%;height:100%;object-fit:contain}
3151.rtv-brand-cons{width:4.6vw;height:100%;min-width:38px;display:flex;align-items:center;justify-content:center;background:var(--rosgan-cons-color);box-sizing:border-box;padding:.7vw;color:#fff;font-size:1.8vw;font-weight:800}
3152.rtv-brand-cons img{display:block!important;width:3.2vw!important;height:3.2vw!important;max-width:100%!important;max-height:100%!important;object-fit:contain!important}
3153.rtv-brand-name{height:100%;box-sizing:border-box;display:flex;flex-direction:column;justify-content:center;gap:.3vw;padding:0 1.25vw;background:#092b49;color:#fff;border-radius:0 .55vw .55vw 0;max-width:25vw}
3154.rtv-brand-name small{font-size:clamp(7px,.55vw,12px);letter-spacing:.17em;color:#9fc0d7;font-weight:700}.rtv-brand-name strong{font-size:clamp(10px,.96vw,21px);font-weight:700;line-height:1.2}
3155.rtv-lower{position:absolute;z-index:3;left:4.5vw;right:4.5vw;bottom:5vh;color:#fff;font-family:Arial,Helvetica,sans-serif;text-shadow:none;filter:drop-shadow(0 8px 22px #00142840)}
3156.rtv-panel{border-radius:.7vw;overflow:hidden;background:#092b49;border-top:.22vw solid #92dfca}
3157.rtv-primary{display:flex;align-items:stretch;min-height:6.5vw}
3158.rtv-number{flex:0 0 7vw;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15vw;background:#123d5b;border-right:1px solid #ffffff1f;padding:.6vw;box-sizing:border-box}
3159.rtv-number small{font-size:clamp(9px,.72vw,16px);font-weight:700;letter-spacing:.16em;color:#b5d6e6}.rtv-number strong{font-size:clamp(26px,3.25vw,70px);font-weight:800;line-height:1;letter-spacing:-.045em;font-variant-numeric:tabular-nums;color:#fff}
3160.rtv-identity{flex:1;min-width:0;padding:1vw 1.6vw;display:flex;flex-direction:column;justify-content:center;gap:.28vw}
3161.rtv-certificate{font-size:clamp(8px,.67vw,15px);font-weight:700;letter-spacing:.1em;color:#92dfca}.rtv-title{margin:0!important;font-size:clamp(19px,2.05vw,46px)!important;font-weight:800!important;line-height:1.08!important;color:#fff!important;letter-spacing:-.025em;overflow-wrap:anywhere}
3162.rtv-breed{font-size:clamp(10px,.98vw,22px);line-height:1.25;color:#c2d8e7;font-weight:500}
3163.rtv-metrics{display:flex;align-items:stretch;flex:0 0 auto;background:#f3f7f8;color:#092b49}
3164.rtv-metric{display:flex;flex-direction:column;justify-content:center;gap:.4vw;min-width:11vw;padding:.8vw 1.7vw;box-sizing:border-box;border-left:1px solid #092b491a}
3165.rtv-metric small{font-size:clamp(8px,.67vw,15px);letter-spacing:.1em;font-weight:700;color:#496576}.rtv-metric strong{font-size:clamp(22px,2.6vw,56px);line-height:1;font-weight:800;letter-spacing:-.035em;white-space:nowrap;font-variant-numeric:tabular-nums}
3166.rtv-secondary{display:flex;align-items:stretch;border-top:1px solid #ffffff20;background:#061f36}
3167.rtv-detail{display:flex;flex-direction:column;justify-content:center;gap:.3vw;padding:.7vw 1.6vw;border-right:1px solid #ffffff1c;min-width:0;flex:1}.rtv-detail:first-child{flex:1.2}.rtv-detail:last-child{flex:.65;border:0}
3168.rtv-detail small{font-size:clamp(7px,.57vw,13px);font-weight:700;letter-spacing:.13em;color:#90b2c8}.rtv-detail strong{font-size:clamp(10px,1.04vw,23px);font-weight:600;line-height:1.2;overflow-wrap:anywhere}
3169.rtv-contacts{display:flex;align-items:center;gap:1.4vw;margin-top:.4vw;padding:.6vw 1.25vw;border-radius:.35vw;background:#f3f7f8;color:#123d5b;font-size:clamp(9px,.92vw,20px);font-weight:700;line-height:1.35}.rtv-contacts-label{flex:none;font-size:clamp(7px,.6vw,13px);letter-spacing:.12em;color:#4b6778}.rtv-contacts-copy{overflow-wrap:anywhere}
3170.rtv-brand sc-if,.rtv-metrics sc-if,.rtv-secondary sc-if{display:contents}.rtv-lower .sc-interp,.rtv-brand .sc-interp{font:inherit;color:inherit;letter-spacing:inherit;line-height:inherit}
3171@media(max-aspect-ratio:1/1){.rtv-lower{left:4vw;right:4vw}.rtv-primary{flex-wrap:wrap}.rtv-number{flex-basis:15%;min-height:70px}.rtv-identity{flex-basis:70%;padding:12px}.rtv-metrics{width:100%}.rtv-metric{flex:1;padding:10px 14px}.rtv-detail{padding:10px;gap:5px}.rtv-brand-name{max-width:45vw}.rtv-brand-rosgan{width:24vw;padding:8px}.rtv-brand-cons{width:38px;padding:6px}.rtv-brand-cons img{width:26px!important;height:26px!important}}
3172/* TV height budget including contacts stays below one sixth of the viewport. */
3173.rtv-lower{max-height:16.6667vh}
3174.rtv-panel{border-top-width:.25vh}
3175.rtv-primary{height:9vh;min-height:0;flex-wrap:nowrap}
3176.rtv-number{min-height:0;flex-basis:7vw;padding:.4vh .6vw;gap:.2vh}
3177.rtv-number small{font-size:1.1vh}.rtv-number strong{font-size:4.5vh}
3178.rtv-identity{flex-basis:auto;padding:.6vh 1.6vw;gap:.2vh;overflow:hidden}
3179.rtv-certificate{font-size:1.05vh}.rtv-title{font-size:3.1vh!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rtv-breed{font-size:1.6vh;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
3180.rtv-metrics{width:auto}.rtv-metric{padding:.6vh 1.7vw;gap:.45vh}.rtv-metric small{font-size:1.05vh}.rtv-metric strong{font-size:3.8vh}
3181.rtv-secondary{height:4.3vh;box-sizing:border-box}
3182.rtv-lower .rtv-metric strong.rtv-quantity{font-size:4vh;line-height:1}
3183.rtv-quantity-breakdown{display:flex;gap:.35em;font-size:3.2vh;font-weight:700;line-height:1;color:#092b49;font-variant-numeric:tabular-nums}
3184.rtv-quantity-breakdown>span+span::before{content:'/';margin-right:.35em}
3185.rtv-quantity-breakdown span{white-space:nowrap}
3186.rtv-primary:has(.rtv-quantity-breakdown){min-height:10.6vh}
3187.rtv-visec-badge{background:#16734e;color:#fff}.rtv-visec-badge img{filter:brightness(0) invert(1)}
3188.rtv-lower:has(.rtv-china-badge:not(.rtv-visec-badge)) .rtv-visec-badge{left:14vw}
3189.rtv-detail{padding:.35vh 1.6vw;gap:.15vh;overflow:hidden}.rtv-detail small{font-size:.9vh}.rtv-detail strong{font-size:1.65vh;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
3190.rtv-contacts{height:2.5vh;box-sizing:border-box;margin-top:.3vh;padding:0 1.25vw;font-size:1.4vh;overflow:hidden}.rtv-contacts-label{font-size:.95vh}.rtv-contacts-copy{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
3191.rtv-lower:not(:has(.rtv-contacts)) .rtv-primary{height:11.8vh}
3192.rtv-lower:not(:has(.rtv-contacts)) .rtv-number strong{font-size:5.3vh}
3193.rtv-lower:not(:has(.rtv-contacts)) .rtv-title{font-size:3.7vh!important}
3194.rtv-lower:not(:has(.rtv-contacts)) .rtv-breed{font-size:1.8vh}
3195.rtv-lower:not(:has(.rtv-contacts)) .rtv-metric strong{font-size:4.5vh}
3196.rtv-secondary{height:5.5vh}
3197.rtv-detail{gap:.25vh}.rtv-detail small{font-size:1.15vh}.rtv-detail strong{font-size:2.2vh}
3198.rtv-primary{height:7.8vh}.rtv-lower:not(:has(.rtv-contacts)) .rtv-primary{height:10.6vh}
3199.rtv-lower:has(.rtv-contacts) .rtv-secondary{height:4.3vh}
3200.rtv-contacts{height:3.7vh;font-size:2.5vh;font-weight:800;line-height:1.1;gap:1vw}
3201.rtv-contacts-label{font-size:1.1vh;letter-spacing:.08em}
3202.rtv-identity{padding:.6vh 2.1vw}
3203.rtv-metric{padding:.6vh 2.1vw}
3204.rtv-detail{flex-direction:row;align-items:center;justify-content:flex-start;gap:.8vw;padding:.75vh 2.1vw;box-sizing:border-box}
3205.rtv-detail small{flex-shrink:0;font-size:1.7vh;line-height:1.2;font-weight:800;letter-spacing:.035em}
3206.rtv-detail strong{min-width:0;font-size:2.2vh;line-height:1.2}
3207.rtv-secondary .rtv-detail--location{flex:1 1 0}.rtv-secondary .rtv-detail--payment{flex:1.5 1 0}
3208.rtv-contacts{padding:.6vh 2.1vw;gap:1.5vw;font-size:2.2vh;line-height:1.1}
3209.rtv-identity{flex-direction:row;align-items:stretch;gap:1.5vw}
3210.rtv-identity-fixed{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:.2vh}
3211.rtv-identity sc-if{display:contents}
3212.rtv-rotating{flex:0 0 42%;min-width:0;box-sizing:border-box;border-left:1px solid #ffffff30;padding:.35vh 0 .35vh 1.5vw;
3212display:flex;flex-direction:column;justify-content:center;gap:.55vh}
3213.rtv-rotating-label{display:flex;align-items:center;justify-content:space-between;gap:1vw;font-size:1.7vh;font-weight:800;line-height:1.15;letter-spacing:.045em;color:#b5f2e1}
3214.rtv-rotating-value{font-size:2.2vh;font-weight:600;line-height:1.2;color:#fff;overflow-wrap:anywhere}
3215.rtv-observations{position:absolute;bottom:calc(100% + .45vh);left:0;right:0;height:3.4vh;display:flex;align-items:center;overflow:hidden;border-radius:.35vw;background:#061f36ed;color:#fff}
3216.rtv-observations-label{align-self:stretch;display:flex;align-items:center;flex-shrink:0;padding:0 1.25vw;background:#123d5b;color:#92dfca;font-size:1.1vh;font-weight:700;letter-spacing:.09em}
3217.rtv-observations-window{min-width:0;flex:1;overflow:hidden}
3218.rtv-observations-track{display:flex;width:max-content;animation:rtv-observations-roll var(--roll-duration,35s) linear infinite;will-change:transform}
3219.rtv-observations-copy{flex:none;white-space:nowrap;padding-right:5vw;font-size:2.2vh;font-weight:600;line-height:1.2}
3220@keyframes rtv-observations-roll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
3221.rosgan-tv-output video[data-rosgan-tv-deck]{opacity:0;z-index:0;pointer-events:none}
3222.rosgan-tv-output video[data-rosgan-tv-deck].is-on-air{opacity:1;z-index:1}
3223.rosgan-tv-output video[data-rosgan-tv-multi-deck]{opacity:0;z-index:2;pointer-events:none}
3224.rosgan-tv-output.is-multi video[data-rosgan-tv-multi-deck].is-on-air{opacity:1}
3225.rosgan-tv-output.is-multi video[data-rosgan-tv-deck]{opacity:0}
3225
3226.ros-tv-air-pane{position:absolute;width:100%;height:100%;transform-origin:top left;z-index:4;pointer-events:none}
3227.rg-commercial-page .commercial-summary sc-for{display:contents}.rg-commercial-page .commercial-hero{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.rg-commercial-head,.rg-commercial-row{display:grid;grid-template-columns:50px minmax(200px,1.45fr) minmax(155px,1fr) minmax(180px,1fr) minmax(180px,1fr);gap:18px;align-items:center;padding:16px 20px}.rg-commercial-head{position:sticky;top:64px;z-index:10;background:#f6f9fc;border-radius:8px 8px 0 0;color:#71839a;font-size:10px;font-weight:850;text-transform:uppercase;letter-spacing:.07em;border-bottom:1px solid #dce7f2}.rg-commercial-row{border-left:3px solid transparent;transition:background .15s}.rg-commercial-row:hover{background:#fbfdff}.rg-commercial-row.is-sold{border-left-color:#17704d}.rg-commercial-row.is-lifted{border-left-color:#94a3b8;background:#f8fafc}.rg-commercial-identity{display:grid;gap:5px;min-width:0}.rg-commercial-identity>small{color:#7a8da1;font-size:10px;font-weight:800}.rg-commercial-identity>strong{font-size:15px;color:#10233b}.rg-commercial-breed{font-size:11px;color:#65758a}.rg-commercial-cons{display:flex;align-items:center;gap:8px;margin-top:3px;color:#526b82;font-size:11px}.rg-commercial-cons .commercial-cons-avatar{flex-shrink:0;box-shadow:none}.rg-commercial-data{display:grid;gap:6px;color:#526b82;font-size:12px;line-height:1.4}.rg-commercial-data strong{color:#173b5c;font-size:13px}.rg-commercial-data small{font-size:11px}.rg-commercial-status{display:grid;gap:8px}.rg-commercial-status>.rosgan-ops-status{justify-self:start}.rg-commercial-status .commercial-status{grid-template-columns:repeat(3,minmax(0,1fr));gap:4px}.rg-commercial-status .commercial-status button{min-height:32px;font-size:10px}.rg-commercial-tools{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}.rg-commercial-search{display:flex;align-items:center;gap:9px;background:#fff;border:1px solid #d8e5f1;border-radius:9px;padding:0 12px;width:min(330px,100%);box-sizing:border-box}.rg-commercial-search img{width:17px;height:17px}.rg-commercial-search input{width:100%;min-height:40px;outline:none;border:0;background:transparent;font:inherit;font-size:12px;color:#173b5c}.rg-commercial-search:focus-within{outline:2px solid #0a5ca8}.rg-commercial-caption{display:flex;justify-content:space-between;gap:12px;margin:14px 0;color:#71839a;font-size:12px}.rg-commercial-caption strong{color:#173b5c}.rg-commercial-page .commercial-price input{font-size:15px;min-height:40px;font-variant-numeric:tabular-nums}.rg-commercial-page .commercial-price{gap:8px}@media(max-width:1050px){.rg-commercial-page .commercial-hero{grid-template-columns:1fr}.rg-commercial-page .commercial-hero-actions{justify-content:flex-start}.rg-commercial-head{display:none}.rg-commercial-row{grid-template-columns:38px minmax(0,1.4fr) minmax(0,1fr);gap:14px}.rg-commercial-status{grid-column:2}.rg-commercial-row>.commercial-price{grid-column:3}.rg-commercial-order{align-self:start}}@media(max-width:600px){.rg-commercial-tools{display:block}.rg-commercial-search{width:100%;margin-bottom:12px}.rg-commercial-row{grid-template-columns:32px minmax(0,1fr);padding:16px 12px;gap:12px}.rg-commercial-data,.rg-commercial-status,.rg-commercial-row>.commercial-price{grid-column:2}.rg-commercial-data{padding:9px 0;border-top:1px solid #e7eef5}.rg-commercial-page .commercial-hero-actions{gap:7px}.rg-commercial-page .commercial-hero-actions button{font-size:11px}}
3228.ros-tv-lot-row.is-lifted{opacity:.5;background:#f1f4f7;cursor:not-allowed}.ros-tv-lot-main:disabled{cursor:not-allowed}.ros-tv-sold-check{display:inline-flex;vertical-align:middle;margin-left:5px;color:#17704d}.ros-tv-lifted-label{margin-left:6px;color:#687b91;font-weight:700}
3229.ros-tv-contacts-section{margin-top:18px}.ros-tv-contact-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.ros-tv-contact-grid sc-for{display:contents}.ros-tv-contact-card{display:grid;gap:9px;padding:16px;border:1px solid #dbe5ef;border-radius:12px;background:#f8fafc}.ros-tv-contact-identity{display:flex;align-items:center;gap:12px;min-width:0;color:#12334e;font-size:14px}.ros-tv-contact-logo{display:grid;place-items:center;flex-shrink:0;width:48px;height:48px;padding:7px;box-sizing:border-box;border-radius:9px;color:#fff;font-weight:800}.ros-tv-contact-logo img{max-width:100%;max-height:100%;object-fit:contain}.ros-tv-contact-caption{font-size:11px;font-weight:700;color:#63788e}.ros-tv-contact-card input{width:100%;box-sizing:border-box;padding:11px;border:1px solid #cad8e6;border-radius:9px;background:#fff;color:#12334e;font:inherit;font-size:13px}.ros-tv-contact-card input:focus{outline:2px solid #0a5ca8;outline-offset:1px}@media(max-width:650px){.ros-tv-contact-grid{grid-template-columns:1fr}}
3230.rosgan-tv-output{background-color:#002e5f;background-image:repeating-linear-gradient(135deg,transparent 0,transparent 55px,rgba(255,255,255,.035) 55px,rgba(255,255,255,.035) 56px),radial-gradient(ellipse at 20% 15%,#084c80 0,transparent 65%)}
3231html[data-rosgan-tv-output="true"] #rosgan-static-boot,html[data-rosgan-tv-output="true"] .rosgan-fast-loader{display:none!important}
3232</style>
3233</helmet>
3234
3235<!-- Permite confirmar el montaje de React aun durante una restauracion de sesion silenciosa. -->
3236<span aria-hidden="true" style="position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap;">Interfaz principal de Rosgan preparada</span>
3237
3238<!-- ===================== APP WELCOME ===================== -->
3239<sc-if value="{{ showConnectingScreen }}" hint-placeholder-val="{{ false }}">
3240  <main class="app-welcome app-connecting-screen" aria-label="Conectando con Rosgan" aria-busy="true">
3241    <div class="app-welcome-content">
3242      <div class="app-welcome-lottie-slot">
3243        <dotlottie-wc class="app-connecting-lottie" data-dc-attr-src="{{ connectingLottieSrc }}" autoplay loop speed="1" aria-label="Conectando con los servidores"></dotlottie-wc>
3244      </div>
3245      <span class="app-welcome-kicker">Conexión segura</span>
3246      <h1>Conectando con Rosgan</h1>
3247      <p class="app-connecting-description" role="status">Estamos preparando tu acceso al mercado ganadero.</p>
3248    </div>
3249  </main>
3250</sc-if>
3251
3252<sc-if value="{{ showWelcomeScreen }}" hint-placeholder-val="{{ false }}">
3253  <main class="app-welcome app-connecting-screen app-connection-success" aria-label="Bienvenida a Rosgan">
3254    <div class="app-welcome-content">
3255      <div class="app-welcome-lottie-slot">
3256        <dotlottie-wc class="app-welcome-lottie" data-dc-attr-src="{{ welcomeLottieSrc }}" autoplay speed="1" aria-label="Acceso confirmado"></dotlottie-wc>
3257      </div>
3258      <span class="app-welcome-kicker">Conexión confirmada</span>
3259      <h1>{{ advanceGreeting }}</h1>
3260      <p class="app-connecting-description" role="status">Ya estás conectado. Bienvenido a Rosgan.</p>
3261    </div>
3262  </main>
3263</sc-if>
3264
3265<sc-if value="{{ authTransitioning }}" hint-placeholder-val="{{ false }}">
3266  <div class="auth-view-bridge" aria-hidden="true"></div>
3267</sc-if>
3268
3269<!-- ===================== AUTH RESTORING ===================== -->
3270<sc-if value="{{ isAuthRestoring }}" hint-placeholder-val="{{ false }}">
3271  <main class="rosgan-fast-loader" aria-label="Cargando Rosgan">
3272    <span class="rosgan-fast-loader-spinner" aria-hidden="true"></span>
3273  </main>
3274</sc-if>
3275
3276<!-- ===================== LOGIN ===================== -->
3277<sc-if value="{{ isLogin }}" hint-placeholder-val="{{ true }}">
3278  <div class="rosgan-auth-screen" style="min-height:100vh; min-height:100svh; position:relative; overflow:hidden; display:grid; place-items:center; padding:24px; box-sizing:border-box;">
3279    <img class="rosgan-auth-background" src="assets/angus-field.png" alt="" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:kenburns 24s ease-in-out infinite alternate;">
3280    <div class="rosgan-auth-shade" style="position:absolute; inset:0; background:linear-gradient(120deg, rgba(0,29,61,0.96) 30%, rgba(0,46,95,0.6) 70%, rgba(10,92,168,0.3));"></div>
3281    <form class="rosgan-auth-card" onSubmit="{{ submitAuthForm }}" style="position:relative; width:min(400px, 100%); background:rgba(255,255,255,0.1); backdrop-filter:blur(22px); -webkit-backdrop-filter:blur(22px); border:1px solid rgba(255,255,255,0.22); border-radius:22px; padding:clamp(26px, 4vw, 38px) clamp(22px, 4vw, 34px); color:#fff; display:flex; flex-direction:column; gap:15px; animation:pageIn 500ms cubic-bezier(0.22,0.72,0.18,1) both;">
3282    <img class="rosgan-auth-logo" src="assets/rosgan-logo-blue.png" alt="Rosgan" style="filter:brightness(0) invert(1); margin:0 auto;">
3283    <div style="text-align:center;">
3284      <div style="font-size:22px; font-weight:800; letter-spacing:-0.01em;">{{ loginTitle }}</div>
3285      <div style="font-size:13px; color:#c8d8ea; margin-top:2px;">{{ loginSubtitle }}</div>
3286    </div>
3287    <sc-if value="{{ showAccountAssistantLauncher }}" hint-placeholder-val="{{ true }}">
3288      <button type="button" class="rosgan-account-help-launcher" onClick="{{ openAccountAssistant }}"><span aria-hidden="true" style="width:21px;height:21px;border:2px solid currentColor;border-radius:50%;display:grid;place-items:center;font-size:13px;line-height:1;font-weight:950;">?</span>Necesito ayuda para crear mi cuenta</button>
3289    </sc-if>
3290    <sc-if value="{{ accountAssistantOpen }}" hint-placeholder-val="{{ false }}">
3291      <section class="rosgan-account-assistant" role="region" aria-live="polite" aria-label="Asistente para crear una cuenta">
3292        <div class="rosgan-account-assistant-head">
3293          <span class="rosgan-account-assistant-avatar" aria-hidden="true">👋</span>
3294          <div class="rosgan-account-assistant-copy"><strong>{{ accountAssistantTitle }}</strong><span>Estoy acá para acompañarte. Vamos de a un paso por vez.</span></div>
3295          <button type="button" class="rosgan-account-assistant-close" onClick="{{ closeAccountAssistant }}" aria-label="Cerrar la ayuda" title="Cerrar">×</button>
3296        </div>
3297        <sc-if value="{{ accountAssistantHasProgress }}" hint-placeholder-val="{{ false }}">
3298          <div class="rosgan-account-assistant-progress"><b>{{ accountAssistantProgressLabel }}</b><span style="--account-help-progress:{{ accountAssistantProgress }};"><i></i></span></div>
3299        </sc-if>
3300        <p class="rosgan-account-assistant-text">{{ accountAssistantText }}</p>
3301        <sc-if value="{{ accountAssistantHasTip }}" hint-placeholder-val="{{ true }}"><div class="rosgan-account-assistant-tip">{{ accountAssistantTip }}</div></sc-if>
3302        <sc-if value="{{ accountAssistantMessage }}" hint-placeholder-val="{{ false }}"><div class="rosgan-account-assistant-error" role="alert">{{ accountAssistantMessage }}</div></sc-if>
3303        <sc-if value="{{ accountAssistantIsChoice }}" hint-placeholder-val="{{ true }}">
3304          <div class="rosgan-account-assistant-actions">
3305            <button type="button" class="rosgan-account-assistant-primary" onClick="{{ chooseAccountAssistantGoogle }}"><img src="assets/google-logo.webp" alt="" style="width:19px;height:19px;vertical-align:middle;background:#fff;border-radius:50%;padding:2px;"> Usar mi cuenta de Google</button>
3306            <button type="button" class="rosgan-account-assistant-secondary" onClick="{{ chooseAccountAssistantEmail }}"><img src="assets/icons/lucide/mail.svg" alt="" style="width:18px;height:18px;vertical-align:middle;"> Usar correo y contraseña</button>
3307          </div>
3308        </sc-if>
3309        <sc-if value="{{ accountAssistantIsGoogle }}" hint-placeholder-val="{{ false }}">
3310          <div class="rosgan-account-assistant-actions">
3311            <button type="button" class="rosgan-account-assistant-secondary" onClick="{{ backAccountAssistant }}">Volver</button>
3312            <button type="button" class="rosgan-account-assistant-primary" onClick="{{ googleAuth }}"><img src="assets/google-logo.webp" alt="" style="width:19px;height:19px;vertical-align:middle;background:#fff;border-radius:50%;padding:2px;"> Continuar con Google</button>
3313          </div>
3314        </sc-if>
3315        <sc-if value="{{ accountAssistantIsEmail }}" hint-placeholder-val="{{ false }}">
3316          <div class="rosgan-account-assistant-actions">
3317            <button type="button" class="rosgan-account-assistant-secondary" onClick="{{ backAccountAssistant }}">{{ accountAssistantBackLabel }}</button>
3318            <button type="button" class="rosgan-account-assistant-primary" onClick="{{ nextAccountAssistant }}" disabled="{{ accountAssistantNextDisabled }}">{{ accountAssistantNextLabel }}</button>
3319          </div>
3320        </sc-if>
3321      </section>
3322    </sc-if>
3323    <sc-if value="{{ showAuthNameField }}" hint-placeholder-val="{{ false }}">
3324      <input class="{{ authNameClass }}" data-account-field="name" value="" data-dc-attr-value="{{ formName }}" onInput="{{ onName }}" onKeyDown="{{ submitAuthOnEnter }}" placeholder="Nombre y apellido" autocomplete="name" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);">
3325    </sc-if>
3326    <sc-if value="{{ showAuthPhoneField }}" hint-placeholder-val="{{ false }}">
3327      <input class="{{ authPhoneClass }}" data-account-field="phone" value="" data-dc-attr-value="{{ formPhone }}" onInput="{{ onFormPhone }}" onKeyDown="{{ submitAuthOnEnter }}" type="tel" inputmode="tel" placeholder="+54 9 341 000 0000" autocomplete="tel" aria-label="Teléfono móvil" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);">
3328    </sc-if>
3329    <sc-if value="{{ showAuthEmailField }}" hint-placeholder-val="{{ true }}">
3330    <input class="{{ authEmailClass }}" data-account-field="email" value="" data-dc-attr-value="{{ formEmail }}" onInput="{{ onEmail }}" onKeyDown="{{ submitAuthOnEnter }}" type="email" placeholder="[email protected]" autocomplete="email" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);">
3331    </sc-if>
3332    <sc-if value="{{ showAuthPasswordField }}" hint-placeholder-val="{{ true }}">
3333    <sc-if value="{{ showPassword }}" hint-placeholder-val="{{ true }}">
3334      <div style="position:relative; width:100%;">
3335        <input class="{{ authPasswordClass }}" data-account-field="password" value="" data-dc-attr-value="{{ formPassword }}" onInput="{{ onPassword }}" onKeyDown="{{ submitAuthOnEnter }}" type="{{ passwordInputType }}" placeholder="Contraseña" autocomplete="{{ passwordAutocomplete }}" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 50px 13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);">
3336        <button type="button" onClick="{{ toggleAuthPasswordVisibility }}" aria-label="{{ passwordVisibilityLabel }}" aria-pressed="{{ authPasswordVisible }}" title="{{ passwordVisibilityLabel }}" style="position:absolute; top:50%; right:6px; width:38px; height:38px; transform:translateY(-50%); display:grid; place-items:center; padding:0; border:0; border-radius:9px; background:transparent; color:#fff; cursor:pointer; opacity:.9;" style-hover="background:rgba(255,255,255,.12); opacity:1;">
3337          <span aria-hidden="true" style="position:relative; width:20px; height:20px; display:grid; place-items:center;"><img src="assets/icons/lucide/eye.svg" alt="" style="width:19px; height:19px; filter:brightness(0) invert(1);"><sc-if value="{{ authPasswordVisible }}" hint-placeholder-val="false"><span style="position:absolute; width:22px; height:2px; border-radius:999px; background:#fff; transform:rotate(-45deg);"></span></sc-if></span>
3338        </button>
3339      </div>
3340      <sc-if value="{{ isRegister }}" hint-placeholder-val="{{ false }}">
3341        <div aria-label="Requisitos de contraseña" style="display:grid; gap:7px; padding:1px 3px 2px; font-size:12px; color:#d6e4f2;">
3342          <div style="display:flex; align-items:center; gap:8px; color:{{ passwordLengthColor }}; transition:color 180ms ease;">
3343            <span aria-hidden="true" style="width:19px; height:19px; display:grid; place-items:center; flex:0 0 19px; border-radius:50%; border:1px solid {{ passwordLengthBorder }}; background:{{ passwordLengthBg }}; color:{{ passwordLengthIconColor }}; font-size:12px; font-weight:950; transition:all 180ms ease;"><sc-if value="{{ passwordLengthValid }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check"></i></sc-if></span>
3344            Mínimo 8 caracteres
3345          </div>
3346          <div style="display:flex; align-items:center; gap:8px; color:{{ passwordUpperColor }}; transition:color 180ms ease;">
3347            <span aria-hidden="true" style="width:19px; height:19px; display:grid; place-items:center; flex:0 0 19px; border-radius:50%; border:1px solid {{ passwordUpperBorder }}; background:{{ passwordUpperBg }}; color:{{ passwordUpperIconColor }}; font-size:12px; font-weight:950; transition:all 180ms ease;"><sc-if value="{{ passwordUpperValid }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check"></i></sc-if></span>
3348            Al menos 1 letra mayúscula
3349          </div>
3350        </div>
3351      </sc-if>
3352    </sc-if>
3353    <sc-if value="{{ showPrimaryAuthSubmit }}" hint-placeholder-val="{{ true }}"><button class="{{ authSubmitClass }}" data-account-field="submit" type="submit" disabled="{{ authBusy }}" style="background:#fff; color:#002e5f; border:none; padding:14px; border-radius:12px; font-weight:700; font-size:15px; font-family:inherit; cursor:pointer; transition:transform 160ms; opacity:{{ authBtnOpacity }};" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.98);">{{ submitLabel }}</button></sc-if>
3354    </sc-if>
3355    <sc-if value="{{ showDefaultGoogleAuth }}" hint-placeholder-val="{{ true }}">
3356      <div style="display:flex; align-items:center; gap:10px;"><span style="flex:1; height:1px; background:rgba(255,255,255,0.2);"></span><span style="font-size:12px; color:#c8d8ea;">o continuá con</span><span style="flex:1; height:1px; background:rgba(255,255,255,0.2);"></span></div>
3357      <button type="button" onClick="{{ googleAuth }}" style="background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:12px; display:flex; justify-content:center; gap:10px; align-items:center; font-size:14px; color:#fff; font-family:inherit; cursor:pointer;" style-hover="background:rgba(255,255,255,0.16);"><img src="assets/google-logo.webp" alt="" style="width:20px; height:20px; background:#fff; border-radius:50%; padding:2px;">Continuar con Google</button>
3358    </sc-if>
3359    <sc-if value="{{ authMessage }}" hint-placeholder-val="{{ false }}">
3360      <div style="font-size:13px; text-align:center; padding:10px 12px; border-radius:10px; background:rgba(230,57,70,0.25); border:1px solid rgba(230,57,70,0.4); color:#ffd3d7;">{{ authMessage }}</div>
3361    </sc-if>
3362    <sc-if value="{{ showAuthFooterLinks }}" hint-placeholder-val="{{ true }}"><div style="display:flex; justify-content:center; gap:18px; font-size:12.5px; color:#c8d8ea;">
3363      <span onClick="{{ toggleReset }}" style="cursor:pointer;" style-hover="color:#ffffff;">{{ resetLink }}</span>
3364      <span onClick="{{ toggleRegister }}" style="cursor:pointer;" style-hover="color:#ffffff;">{{ registerLink }}</span>
3365    </div></sc-if>
3366    </form>
3367  </div>
3368</sc-if>
3369
3370<!-- ===================== EMAIL VERIFICATION ===================== -->
3371<sc-if value="{{ isEmailVerification }}" hint-placeholder-val="{{ false }}">
3372  <div class="rosgan-auth-screen" style="min-height:100vh; min-height:100svh; position:relative; overflow:hidden; display:grid; place-items:center; padding:24px; box-sizing:border-box;">
3373    <img class="rosgan-auth-background" src="assets/angus-field.png" alt="" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:kenburns 24s ease-in-out infinite alternate;">
3374    <div class="rosgan-auth-shade" style="position:absolute; inset:0; background:linear-gradient(120deg, rgba(0,29,61,0.97) 30%, rgba(0,46,95,0.72) 70%, rgba(10,92,168,0.42));"></div>
3375    <section class="rosgan-auth-card" aria-labelledby="email-verification-title" style="position:relative; width:min(440px, 100%); box-sizing:border-box; background:rgba(255,255,255,0.11); backdrop-filter:blur(22px); -webkit-backdrop-filter:blur(22px); border:1px solid rgba(255,255,255,0.24); border-radius:22px; padding:clamp(26px, 4vw, 38px) clamp(22px, 4vw, 34px); color:#fff; display:flex; flex-direction:column; gap:16px; animation:pageIn 500ms cubic-bezier(0.22,0.72,0.18,1) both; box-shadow:0 28px 70px rgba(0,14,34,.32);">
3376      <img class="rosgan-auth-logo" src="assets/rosgan-logo-blue.png" alt="Rosgan" style="filter:brightness(0) invert(1); margin:0 auto 2px;">
3377      <div style="width:58px; height:58px; display:grid; place-items:center; align-self:center; border-radius:50%; background:rgba(255,255,255,.13); border:1px solid rgba(255,255,255,.26);"><img src="assets/icons/lucide/mail.svg" alt="" style="width:26px;height:26px;filter:brightness(0) invert(1);"></div>
3378      <div style="text-align:center;">
3379        <div id="email-verification-title" style="font-size:24px; font-weight:850; letter-spacing:-0.02em;">Verificá tu correo</div>
3380        <div style="font-size:13px; line-height:1.55; color:#c8d8ea; margin-top:6px;">Enviamos un enlace de confirmación a <strong style="color:#fff;">{{ emailVerificationEmail }}</strong>. Abrilo y después volvé acá: lo comprobaremos automáticamente.</div>
3381      </div>
3382      <sc-if value="{{ accountAssistantOpen }}" hint-placeholder-val="{{ false }}">
3383        <div class="rosgan-account-verification-help" role="status"><strong>Te acompaño en este paso</strong><span>1. Abrí tu correo. 2. Buscá el mensaje de Rosgan. 3. Tocá el enlace de confirmación. 4. Volvé a esta pantalla y presioná “Ya verifiqué mi correo”. Si no aparece, revisá Spam.</span><button type="button" class="rosgan-account-assistant-close" onClick="{{ closeAccountAssistant }}" aria-label="Cerrar la ayuda" style="justify-self:end;">×</button></div>
3384      </sc-if>
3385      <button onClick="{{ checkEmailVerification }}" disabled="{{ emailVerificationBusy }}" style="background:#fff; color:#002e5f; border:none; padding:14px; border-radius:12px; font-weight:800; font-size:15px; font-family:inherit; cursor:pointer; opacity:{{ emailVerificationOpacity }};">{{ emailVerificationCheckLabel }}</button>
3386      <button onClick="{{ resendEmailVerification }}" disabled="{{ emailVerificationBusy }}" style="background:transparent; color:#e4f0fb; border:1px solid rgba(255,255,255,.3); padding:11px; border-radius:12px; font-weight:750; font-size:13px; font-family:inherit; cursor:pointer; opacity:{{ emailVerificationOpacity }};">{{ emailVerificationResendLabel }}</button>
3387      <sc-if value="{{ emailVerificationMessage }}" hint-placeholder-val="{{ false }}">
3388        <div role="status" style="font-size:13px; line-height:1.45; text-align:center; padding:10px 12px; border-radius:10px; background:rgba(22,184,133,0.18); border:1px solid rgba(95,230,187,0.34); color:#e6fff7;">{{ emailVerificationMessage }}</div>
3389      </sc-if>
3390      <div style="font-size:11.5px; line-height:1.45; text-align:center; color:#b9cce0;">Si no lo encontrás, revisá Spam o Correo no deseado.</div>
3391      <button onClick="{{ logoutFromEmailVerification }}" disabled="{{ emailVerificationBusy }}" style="align-self:center; background:none; border:0; color:#c8d8ea; font:700 12px inherit; cursor:pointer; text-decoration:underline; text-underline-offset:3px;">Salir y usar otra cuenta</button>
3392    </section>
3393  </div>
3394</sc-if>
3395
3396<!-- ===================== PHONE VERIFICATION ===================== -->
3397<sc-if value="{{ isPhoneVerification }}" hint-placeholder-val="{{ false }}">
3398  <div class="rosgan-auth-screen" style="min-height:100vh; min-height:100svh; position:relative; overflow:hidden; display:grid; place-items:center; padding:24px; box-sizing:border-box;">
3399    <img class="rosgan-auth-background" src="assets/angus-field.png" alt="" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:kenburns 24s ease-in-out infinite alternate;">
3400    <div class="rosgan-auth-shade" style="position:absolute; inset:0; background:linear-gradient(120deg, rgba(0,29,61,0.97) 30%, rgba(0,46,95,0.72) 70%, rgba(10,92,168,0.42));"></div>
3401    <section class="rosgan-auth-card" aria-labelledby="phone-verification-title" style="position:relative; width:min(440px, 100%); box-sizing:border-box; background:rgba(255,255,255,0.11); backdrop-filter:blur(22px); -webkit-backdrop-filter:blur(22px); border:1px solid rgba(255,255,255,0.24); border-radius:22px; padding:clamp(26px, 4vw, 38px) clamp(22px, 4vw, 34px); color:#fff; display:flex; flex-direction:column; gap:16px; animation:pageIn 500ms cubic-bezier(0.22,0.72,0.18,1) both; box-shadow:0 28px 70px rgba(0,14,34,.32);">
3402      <img class="rosgan-auth-logo" src="assets/rosgan-logo-blue.png" alt="Rosgan" style="filter:brightness(0) invert(1); margin:0 auto 2px;">
3403      <div style="text-align:center;">
3404        <div id="phone-verification-title" style="font-size:24px; font-weight:850; letter-spacing:-0.02em;">Ingresá tu teléfono</div>
3405        <div style="font-size:13px; line-height:1.55; color:#c8d8ea; margin-top:6px;">Guardaremos este número como teléfono de contacto para tu cuenta.</div>
3406      </div>
3407      <sc-if value="{{ isPhoneEntryStep }}" hint-placeholder-val="{{ true }}">
3408        <label style="display:grid; gap:7px; font-size:12px; font-weight:800; color:#eaf3fc;">
3409          Número móvil en formato internacional
3410          <input value="" data-dc-attr-value="{{ phoneVerificationNumber }}" onInput="{{ onPhoneVerificationNumber }}" type="tel" inputmode="tel" placeholder="+54 9 341 000 0000" autocomplete="tel" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.24); border-radius:12px; padding:13px 14px; font-size:15px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.72);">
3411        </label>
3412        <button onClick="{{ savePhoneAndContinue }}" disabled="{{ phoneVerificationBusy }}" style="background:#fff; color:#002e5f; border:none; padding:14px; border-radius:12px; font-weight:800; font-size:15px; font-family:inherit; cursor:pointer; opacity:{{ phoneVerificationOpacity }};">{{ phoneVerificationSendLabel }}</button>
3413      </sc-if>
3414      <sc-if value="{{ phoneVerificationMessage }}" hint-placeholder-val="{{ false }}">
3415        <div role="alert" style="font-size:13px; line-height:1.45; text-align:center; padding:10px 12px; border-radius:10px; background:rgba(230,57,70,0.24); border:1px solid rgba(255,121,131,0.42); color:#ffe1e4;">{{ phoneVerificationMessage }}</div>
3416      </sc-if>
3417      <button onClick="{{ logoutFromPhoneVerification }}" disabled="{{ phoneVerificationBusy }}" style="align-self:center; background:none; border:0; color:#c8d8ea; font:700 12px inherit; cursor:pointer; text-decoration:underline; text-underline-offset:3px;">Salir y usar otra cuenta</button>
3418    </section>
3419  </div>
3420</sc-if>
3421
3422<!-- ===================== APP SHELL ===================== -->
3423<sc-if value="{{ isApp }}" hint-placeholder-val="{{ false }}">
3424  <div style="width:100%; max-width:100vw; min-width:0; min-height:100vh; overflow-x:clip; padding-bottom:{{ homePadBottom }};">
3425
3426    <!-- LIGHT TOPBAR (pool / lot) -->
3427    <sc-if value="{{ showLightBar }}" hint-placeholder-val="{{ false }}">
3428      <div class="app-light-topbar" style="position:sticky; top:0; z-index:40; background:rgba(255,255,255,0.9); backdrop-filter:blur(16px); border-bottom:1px solid #e3eaf2; display:flex; align-items:center; justify-content:space-between; padding:12px clamp(16px, 4vw, 36px); gap:12px;">
3429        <img src="assets/rosgan-logo-blue.png" alt="Rosgan" style="height:{{ logoH }}; cursor:pointer;" onClick="{{ goHome }}">
3430        <sc-if value="{{ desktopNavigation }}" hint-placeholder-val="{{ true }}">
3431          <nav style="display:flex; gap:4px; background:#eef3f9; border:1px solid #e3eaf2; border-radius:100px; padding:5px; font-size:13.5px;">
3432            <sc-for list="{{ navItems }}" as="n" hint-placeholder-count="4">
3433              <span onClick="{{ n.go }}" style="padding:8px 18px; border-radius:100px; cursor:pointer; background:{{ n.lightBg }}; color:{{ n.lightColor }}; font-weight:{{ n.weight }}; transition:background 200ms;" style-hover="background:#dde7f2;">{{ n.label }}</span>
3434            </sc-for>
3435          </nav>
3436        </sc-if>
3437        <div style="display:flex; align-items:center; gap:12px;">
3438          <button onClick="{{ toggleNotifications }}" title="{{ notificationBellTitle }}" aria-label="Notificaciones" style="position:relative; width:38px; height:38px; border-radius:50%; border:1px solid #d9e3ee; background:{{ notificationBellBg }}; display:grid; place-items:center; cursor:pointer; padding:0; font-family:inherit;" style-hover="background:#e6eef7;">
3439            <img src="assets/icons/lucide/bell.svg" alt="" style="width:18px; height:18px;">
3440            <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}">
3441              <span style="position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; padding:0 4px; box-sizing:border-box; border-radius:9px; background:#e63946; color:#fff; border:2px solid #fff; display:grid; place-items:center; font-size:9px; line-height:1; font-weight:800;">{{ notificationsUnreadLabel }}</span>
3442            </sc-if>
3443          </button>
3444          <sc-if value="{{ showDesktopLoad }}" hint-placeholder-val="{{ false }}">
3445            <button onClick="{{ goLotDestination }}" style="display:inline-flex; align-items:center; gap:8px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:10px 20px; font-size:13.5px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; box-shadow:0 8px 22px rgba(230,57,70,0.36); transition:transform 160ms, box-shadow 160ms;" style-hover="transform:translateY(-2px); box-shadow:0 12px 28px rgba(230,57,70,0.46);" style-active="transform:scale(0.97);"><span style="font-size:16px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button>
3446          </sc-if>
3447          <span onClick="{{ goProfile }}" title="Mi perfil" style="width:36px; height:36px; border-radius:50%; background:{{ userAvatarBg }}; border:1px solid {{ userAvatarBorder }}; color:#fff; display:grid; place-items:center; font-size:13px; font-weight:700; cursor:pointer; overflow:hidden;" style-hover="transform:translateY(-1px);">
3448            <sc-if value="{{ userHasConsignatariaAvatar }}" hint-placeholder-val="false"><img data-profile-avatar="true" data-dc-attr-src="{{ userConsignatariaAvatar }}" alt="{{ userConsignatariaName }}" title="Ir a mi perfil" style="width:{{ userAvatarNavSize }}; height:{{ userAvatarNavSize }}; object-fit:{{ userAvatarObjectFit }}; object-position:center; cursor:pointer;"></sc-if>
3449            <sc-if value="{{ userShowsInitials }}" hint-placeholder-val="true">{{ userInitials }}</sc-if>
3450          </span>
3451        </div>
3452      </div>
3453    </sc-if>
3454
3455    <sc-if data-public-blocked-overlay value="{{ notificationsOpen }}" hint-placeholder-val="{{ false }}">
3456      <section style="position:fixed; z-index:70; top:68px; right:clamp(12px, 3vw, 36px); width:min(390px, calc(100vw - 24px)); max-height:min(620px, calc(100vh - 88px)); overflow:hidden; background:#fff; border:1px solid #dbe5ef; border-radius:8px; box-shadow:0 22px 60px rgba(0,46,95,0.22); display:flex; flex-direction:column;">
3457        <header style="padding:16px 16px 13px; display:flex; align-items:center; justify-content:space-between; gap:12px; border-bottom:1px solid #e7edf4;">
3458          <div>
3459            <div style="font-size:16px; color:#002e5f; font-weight:800;">Notificaciones</div>
3460            <div style="font-size:11.5px; color:#718197; margin-top:2px;">Actividad reciente de tu cuenta</div>
3461          </div>
3462          <div style="display:flex; align-items:center; gap:10px;">
3463            <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}">
3464              <button onClick="{{ markAllNotificationsRead }}" style="border:none; background:transparent; color:#0a5ca8; font-size:11px; font-weight:800; font-family:inherit; cursor:pointer; padding:5px;">Marcar leídas</button>
3465            </sc-if>
3466      <button onClick="{{ toggleNotifications }}" aria-label="Cerrar notificaciones" title="Cerrar" style="width:30px; height:30px; border:1px solid #dbe5ef; background:#f5f8fb; border-radius:50%; color:#334b67; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="" style="width:16px;height:16px;"></button>
3467          </div>
3468        </header>
3469        <div style="overflow-y:auto; overscroll-behavior:contain;">
3470          <sc-if value="{{ notificationsLoading }}" hint-placeholder-val="{{ false }}">
3471            <div style="padding:34px 18px; color:#718197; text-align:center; font-size:13px;">Cargando notificaciones...</div>
3472          </sc-if>
3473          <sc-if value="{{ notificationsHasError }}" hint-placeholder-val="{{ false }}">
3474            <div style="margin:14px; padding:12px; border-radius:7px; background:#fff0f1; color:#a42632; font-size:12.5px; font-weight:700;">{{ notificationsError }}</div>
3475          </sc-if>
3476          <sc-if value="{{ notificationsEmpty }}" hint-placeholder-val="{{ false }}">
3477            <div style="padding:42px 22px; text-align:center;">
3478              <img src="assets/icons/lucide/bell-ring.svg" alt="" style="width:28px; height:28px; opacity:0.42;">
3479              <div style="font-size:14px; color:#002e5f; font-weight:800; margin-top:10px;">Todavía no hay notificaciones</div>
3480              <div style="font-size:12px; color:#718197; margin-top:4px;">Los avisos de Roslot aparecerán acá.</div>
3481            </div>
3482          </sc-if>
3483          <sc-for list="{{ notificationItems }}" as="notification" hint-placeholder-count="3">
3484            <div onClick="{{ notification.open }}" style="display:grid; grid-template-columns:36px minmax(0,1fr) 8px; gap:11px; align-items:start; padding:14px 16px; border-bottom:1px solid #edf1f5; background:{{ notification.bg }}; cursor:pointer;" style-hover="background:#f2f7fc;">
3485              <span style="width:36px; height:36px; border-radius:{{ notification.iconRadius }}; display:grid; place-items:center; overflow:hidden; background:{{ notification.iconBg }};">
3486                <img data-dc-attr-src="{{ notification.icon }}" alt="{{ notification.iconLabel }}" style="width:{{ notification.iconSize }}; height:{{ notification.iconSize }}; object-fit:contain;">
3487              </span>
3488              <span style="min-width:0;">
3489                <strong style="display:block; font-size:13px; line-height:1.3; color:#002e5f;">{{ notification.title }}</strong>
3490                <span style="display:block; font-size:12px; line-height:1.4; color:#596c83; margin-top:3px;">{{ notification.body }}</span>
3491                <span style="display:block; font-size:10.5px; color:#8a98aa; margin-top:7px;">{{ notification.time }}</span>
3492              </span>
3493              <span style="width:7px; height:7px; border-radius:50%; background:{{ notification.dotBg }}; margin-top:6px;"></span>
3494            </div>
3495          </sc-for>
3496        </div>
3497        <footer style="padding:11px 14px; border-top:1px solid #e7edf4; background:#f8fafc;">
3498          <button onClick="{{ goNotificationCenter }}" style="width:100%; border:1px solid #ccd9e7; background:#fff; color:#002e5f; border-radius:7px; padding:10px 12px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer;">Ir al centro de notificaciones</button>
3499        </footer>
3500      </section>
3501    </sc-if>
3502
3503    <sc-if value="{{ isNotifications }}" hint-placeholder-val="{{ false }}">
3504      <main style="max-width:980px; margin:0 auto; padding:clamp(24px, 4vw, 46px) clamp(14px, 3vw, 26px) 90px; animation:pageIn 320ms ease;">
3505        <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:18px; margin-bottom:22px; flex-wrap:wrap;">
3506          <div>
3507            <div style="font-size:10px; letter-spacing:0.16em; color:#0a5ca8; font-weight:800; text-transform:uppercase;">Mi actividad</div>
3508            <h1 style="font-size:clamp(26px, 4vw, 38px); line-height:1.05; color:#002e5f; margin:7px 0 6px;">Centro de notificaciones</h1>
3509            <p style="margin:0; color:#65758a; font-size:13.5px;">Avisos nuevos y actividad anterior de tu cuenta.</p>
3510          </div>
3511          <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}">
3512            <button onClick="{{ markAllNotificationsRead }}" style="border:1px solid #cbd9e7; background:#fff; color:#0a5ca8; border-radius:7px; padding:10px 14px; font-size:12px; font-weight:800; font-family:inherit; cursor:pointer;">Marcar todas como leídas</button>
3513          </sc-if>
3514        </div>
3515
3516        <section style="background:#fff; border:1px solid #dbe5ef; border-radius:8px; overflow:hidden; box-shadow:0 14px 34px rgba(0,46,95,0.08);">
3517          <sc-if value="{{ notificationHistoryLoading }}" hint-placeholder-val="{{ false }}">
3518            <div style="padding:48px 20px; text-align:center; color:#718197; font-size:13px;">Cargando historial...</div>
3519          </sc-if>
3520          <sc-if value="{{ notificationHistoryHasError }}" hint-placeholder-val="{{ false }}">
3521            <div style="margin:16px; padding:13px; border-radius:7px; background:#fff0f1; color:#a42632; font-size:13px; font-weight:700;">{{ notificationHistoryError }}</div>
3522          </sc-if>
3523          <sc-if value="{{ notificationHistoryEmpty }}" hint-placeholder-val="{{ false }}">
3524            <div style="padding:58px 22px; text-align:center;">
3525              <img src="assets/icons/lucide/bell-ring.svg" alt="" style="width:34px; height:34px; opacity:0.4;">
3526              <div style="font-size:16px; color:#002e5f; font-weight:800; margin-top:12px;">No hay actividad para mostrar</div>
3527              <div style="font-size:12.5px; color:#718197; margin-top:4px;">Cuando ocurra algo importante en Roslot aparecerá acá.</div>
3528            </div>
3529          </sc-if>
3530          <sc-for list="{{ notificationHistoryItems }}" as="notification" hint-placeholder-count="5">
3531            <div onClick="{{ notification.open }}" style="display:grid; grid-template-columns:42px minmax(0,1fr) auto; gap:13px; align-items:center; padding:16px clamp(14px, 2.5vw, 22px); border-bottom:1px solid #edf1f5; background:{{ notification.bg }}; cursor:pointer;" style-hover="background:#f3f7fb;">
3532              <span style="width:42px; height:42px; border-radius:{{ notification.iconRadius }}; display:grid; place-items:center; overflow:hidden; background:{{ notification.iconBg }};">
3533                <img data-dc-attr-src="{{ notification.icon }}" alt="{{ notification.iconLabel }}" style="width:{{ notification.historyIconSize }}; height:{{ notification.historyIconSize }}; object-fit:contain;">
3534              </span>
3535              <span style="min-width:0;">
3536                <strong style="display:block; color:#002e5f; font-size:14px; line-height:1.3;">{{ notification.title }}</strong>
3537                <span style="display:block; color:#596c83; font-size:12.5px; line-height:1.4; margin-top:3px;">{{ notification.body }}</span>
3538                <span style="display:block; color:#8a98aa; font-size:10.5px; margin-top:7px;">{{ notification.time }}</span>
3539              </span>
3540              <span style="background:{{ notification.statusBg }}; color:{{ notification.statusColor }}; border-radius:100px; padding:5px 9px; font-size:10px; font-weight:800;">{{ notification.status }}</span>
3541            </div>
3542          </sc-for>
3543        </section>
3544      </main>
3545    </sc-if>
3546
3547    <style>
3548      .home-agenda-flyer{scroll-snap-align:start;background:#fff;border:1px solid #dce5ed;border-radius:20px;overflow:hidden;color:#112d47;display:flex;flex-direction:column;box-shadow:0 8px 24px #002e5f0c;transition:transform .2s,box-shadow .2s}
3549      .home-agenda-flyer:hover{transform:translateY(-3px);box-shadow:0 14px 30px #002e5f20;color:#112d47}
3550      .home-agenda-flyer:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px}
3551      .home-agenda-flyer .home-month-card-media{height:156px!important;background:#082842;flex-shrink:0}
3552      .home-agenda-flyer [data-drag-img]{opacity:.8!important;filter:none!important}
3553      .home-agenda-flyer [data-drag-img]+div{background:linear-gradient(180deg,#00172a44,#00172add)!important}
3554      .home-agenda-flyer .home-month-card-logos{top:auto!important;bottom:18px;left:20px!important;transform:none!important;justify-content:flex-start!important;width:80%!important;height:44px!important}
3555      .home-flyer-body{padding:20px;display:flex;flex-direction:column;gap:12px;flex:1}
3556      .home-flyer-schedule{display:flex;gap:12px;align-items:center}
3557      .home-flyer-date{display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:58px;min-height:64px;border-radius:12px;background:#edf4f9;color:#003866;line-height:1.1}
3558      .home-flyer-date strong{font-size:27px;font-weight:850}.home-flyer-date>span{text-transform:uppercase;font-size:11px;font-weight:800;margin-top:3px}
3559      .home-flyer-schedule>span:last-child{display:flex;flex-direction:column;gap:5px;font-size:14px}.home-flyer-schedule>span:last-child>span{color:#64758a;font-size:12px;line-height:1.4}
3560      .home-flyer-title{font-size:19px;font-weight:800;line-height:1.25;letter-spacing:-.025em;overflow-wrap:anywhere}
3561      .home-flyer-cons{font-size:12px;line-height:1.4;color:#64758a}
3562      .home-flyer-cta{margin-top:auto;padding-top:16px;border-top:1px solid #e8edf2;display:flex;justify-content:space-between;align-items:center;font-weight:750;font-size:13px;color:#07569b}.home-flyer-cta img{width:18px;height:18px}
3563      .home-hero{height:auto!important;min-height:clamp(620px,55vw,700px)!important;box-sizing:border-box}
3564      .home-hero-content{height:auto;min-height:min-content;flex-shrink:0!important}
3565      .home-hero-main{flex-shrink:0}
3566      @media(max-width:700px){
3567        body:has(.home-hero) .app-mobile-load-fab{display:none!important}
3568        .home-hero{aspect-ratio:auto!important;height:auto!important;min-height:clamp(680px,125vw,740px)!important;padding-bottom:0!important}
3569        .home-hero [data-hero-img]{inset:62px 0 auto!important;height:clamp(200px,68vw,330px)!important;transform:none!important;will-change:auto!important}
3570        .home-hero [data-hero-img]>div{inset:0!important;background-size:contain!important;background-position:center!important;background-repeat:no-repeat!important;animation:heroImageReveal 900ms ease both!important}
3571        .home-hero-content{height:auto!important;overflow:visible!important}
3572        .home-hero-main{margin-top:calc(clamp(200px,68vw,330px) + 8px)!important;flex:0 0 auto!important;justify-content:flex-start!important;padding:24px 18px 20px!important;gap:0!important;overflow:visible!important}
3573        .home-hero-brand{width:190px!important;height:42px!important;margin-bottom:18px!important}
3574        .home-hero-badge{font-size:10px!important;padding:6px 10px!important}
3575        .home-hero-title{display:block!important;max-width:100%!important;font-size:clamp(26px,7vw,34px)!important;line-height:1.12!important;margin:14px 0 12px!important;overflow:visible!important}
3576        .home-hero-subtitle{display:block!important;font-size:13px!important;white-space:normal!important;line-height:1.5!important;min-height:0!important}
3577        .home-hero-actions{margin-top:20px!important;gap:10px!important}.home-hero-actions>a,.home-hero-actions>span{font-size:13px!important;padding:12px 16px!important;border-radius:10px!important;min-height:44px;box-sizing:border-box}
3578        .home-hero-indices{display:none!important}
3579      }
3580    </style>
3581    <style>
3582      .home-hero-main{touch-action:pan-y;user-select:none;cursor:grab}.home-hero-main:active{cursor:grabbing}
3583      .home-hero-copy{position:relative}
3584      .home-flyer-brand-name{font-size:16px;line-height:1.2;font-weight:800;color:#fff;text-shadow:0 2px 8px #00172a;overflow-wrap:anywhere}
3585      .home-agenda-flyer .home-month-card-logos{width:calc(100% - 40px)!important;height:auto!important;min-height:48px}
3586    </style>
3587    <style>
3588      .home-report-track{display:grid;grid-auto-flow:column;grid-auto-columns:clamp(280px,29vw,350px);
3588gap:18px;overflow-x:auto;padding:4px 2px 18px;scroll-snap-type:x mandatory;scrollbar-width:thin;scrollbar-color:#c6d6e3 transparent}
3589      .home-report-card{scroll-snap-align:start;display:flex;flex-direction:column;align-items:flex-start;gap:18px;min-height:300px;box-sizing:border-box;padding:24px;background:#fff;border:1px solid #dfe7ed;border-radius:18px;color:#102f49;box-shadow:0 4px 16px #17395406;transition:transform .2s,border-color .2s,box-shadow .2s}
3590      .home-report-card:hover{transform:translateY(-3px);border-color:#83b6c3;box-shadow:0 12px 28px #17395412;color:#102f49}
3591      .home-report-card:focus-visible{outline:3px solid #0a5ca8;outline-offset:2px}
3592      .home-report-meta{display:flex;align-items:center;gap:12px;font-size:12px;color:#667d90;font-weight:600;width:100%}
3593      .home-report-icon{display:grid;place-items:center;width:38px;height:42px;border-radius:9px;background:#eff5f8;flex:none}.home-report-icon img{width:20px;height:20px;opacity:.65}
3594      .home-report-latest{color:#137266;background:#edf8f4;padding:5px 9px;border-radius:6px;font-size:10px;letter-spacing:.05em;font-weight:750;text-transform:uppercase}
3595      .home-report-card h3{font-size:20px;line-height:1.35;letter-spacing:-.025em;font-weight:750;margin:0 0 8px;overflow-wrap:anywhere;text-wrap:pretty}
3596      .home-report-footer{margin-top:auto;border-top:1px solid #e8eef2;padding-top:16px;display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;font-size:13px;font-weight:750;color:#0a5ca8}
3597      .home-report-arrow{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:#eef5f9}.home-report-arrow img{width:16px;height:16px}
3598      @media(max-width:700px){.home-report-track{grid-auto-columns:min(84vw,330px);gap:14px}.home-report-card{padding:22px;min-height:290px}.home-report-card h3{font-size:19px}}
3599      @media(prefers-reduced-motion:reduce){.home-report-card{transition:none}}
3600    </style>
3601    <style>
3602.remate-past-toggle{display:flex;align-items:center;gap:14px;width:fit-content;max-width:100%;min-height:60px;margin:28px auto 22px;padding:10px 20px 10px 10px;border:1px solid #d3e3ee;border-radius:16px;background:#fff;color:#063963;font-family:inherit;font-size:15px;font-weight:750;cursor:pointer;box-shadow:0 6px 20px #00345d0c;transition:background .2s,box-shadow .2s,transform .2s}
3603.remate-past-toggle:hover{background:#f0f7fb;box-shadow:0 10px 24px #00345d18;transform:translateY(-2px)}
3604.remate-past-toggle:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px}
3605.remate-past-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:#e9f3fa}.remate-past-icon img{width:20px;height:20px}
3606.remate-past-chevron{width:18px;height:18px;margin-left:12px;transition:transform .2s}.remate-past-toggle[aria-expanded="true"] .remate-past-chevron{transform:rotate(180deg)}
3607@media(max-width:600px){.remate-past-toggle{width:100%;justify-content:flex-start}.remate-past-chevron{margin-left:auto}}
3608    </style>
3609    <link rel="stylesheet" href="assets/congreso-2026/banner.css">
3610    <link rel="stylesheet" href="assets/home-hero-mobile.css">
3611    <!-- ============ HOME ============ -->
3612    <sc-if value="{{ isHome }}" hint-placeholder-val="{{ false }}">
3613      <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1);">
3614      <div class="home-hero {{ heroCongressClass }}" style="position:relative;overflow:hidden;display:flex;flex-direction:column;background:#002e5f;padding-bottom:{{ heroPadBottom }};">
3615        <sc-if value="{{ heroIsAuction }}">        <sc-if value="{{ homeHeroReady }}">
3616          <div data-hero-img style="position:absolute; inset:-2%; transform:translateY({{ parallaxY }}); will-change:transform;">
3617            <img class="home-hero-photo" key="{{ heroTextKey }}" data-dc-attr-src="{{ heroImage }}" alt="" fetchpriority="high" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:{{ heroImagePosition }};--home-hero-mobile-position:{{ heroImageMobilePosition }};--home-hero-mobile-height:{{ heroImageMobileHeight }};animation:heroImageReveal 900ms ease both,kenburns 18s ease-in-out infinite alternate;">
3618          </div>
3619          <div class="home-hero-overlay" style="position:absolute; inset:0; background:{{ heroOverlay }};"></div>
3620        </sc-if>
3621        <div style="position:absolute; inset:0; background:rgba(0,0,0,.07); pointer-events:none;"></div>
3622</sc-if>
3623        <div class="home-hero-content" style="position:relative; flex:1; display:flex; flex-direction:column;">
3624          <div class="home-hero-nav" style="display:flex; align-items:center; justify-content:space-between; padding:18px clamp(16px, 4vw, 36px); gap:12px;">
3625            <img src="assets/rosgan-logo-blue.png" alt="Rosgan" style="height:{{ logoH }}
3625; filter:brightness(0) invert(1);">
3626            <sc-if value="{{ desktopNavigation }}" hint-placeholder-val="{{ true }}">
3627              <nav style="display:flex; gap:4px; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.16); border-radius:100px; padding:5px; font-size:13.5px; color:#c8d8ea;">
3628                <sc-for list="{{ navItems }}" as="n" hint-placeholder-count="4">
3629                  <span onClick="{{ n.go }}" style="padding:8px 18px; border-radius:100px; cursor:pointer; background:{{ n.bg }}; color:{{ n.color }}; font-weight:{{ n.weight }}; transition:background 200ms;" style-hover="background:rgba(255,255,255,0.18);">{{ n.label }}</span>
3630                </sc-for>
3631              </nav>
3632            </sc-if>
3633            <div style="display:flex; align-items:center; gap:12px;">
3634              <button onClick="{{ toggleNotifications }}" title="{{ notificationBellTitle }}" aria-label="Notificaciones" style="position:relative; width:38px; height:38px; border-radius:50%; border:1px solid rgba(255,255,255,0.3); background:rgba(255,255,255,0.14); display:grid; place-items:center; cursor:pointer; padding:0;" style-hover="background:rgba(255,255,255,0.24);">
3635                <img src="assets/icons/lucide/bell.svg" alt="" style="width:18px; height:18px; filter:brightness(0) invert(1);">
3636                <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}">
3637                  <span style="position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; padding:0 4px; box-sizing:border-box; border-radius:9px; background:#e63946; color:#fff; border:2px solid #174f88; display:grid; place-items:center; font-size:9px; line-height:1; font-weight:800;">{{ notificationsUnreadLabel }}</span>
3638                </sc-if>
3639              </button>
3640              <sc-if value="{{ showDesktopLoad }}" hint-placeholder-val="{{ false }}">
3641                <button onClick="{{ goLotDestination }}" style="display:inline-flex; align-items:center; gap:8px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:10px 20px; font-size:13.5px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; box-shadow:0 8px 22px rgba(230,57,70,0.45); transition:transform 160ms, box-shadow 160ms;" style-hover="transform:translateY(-2px); box-shadow:0 12px 30px rgba(230,57,70,0.6);" style-active="transform:scale(0.97);"><span style="font-size:16px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button>
3642              </sc-if>
3643              <span onClick="{{ goProfile }}" title="Mi perfil" style="width:36px; height:36px; border-radius:50%; background:{{ userAvatarBg }}; border:1px solid {{ userAvatarBorder }}; color:#fff; display:grid; place-items:center; font-size:13px; font-weight:700; cursor:pointer; overflow:hidden;" style-hover="transform:translateY(-1px);">
3644                <sc-if value="{{ userHasConsignatariaAvatar }}" hint-placeholder-val="false"><img data-profile-avatar="true" data-dc-attr-src="{{ userConsignatariaAvatar }}" alt="{{ userConsignatariaName }}" title="Ir a mi perfil" style="width:{{ userAvatarNavSize }}; height:{{ userAvatarNavSize }}; object-fit:{{ userAvatarObjectFit }}; object-position:center; cursor:pointer;"></sc-if>
3645                <sc-if value="{{ userShowsInitials }}" hint-placeholder-val="true">{{ userInitials }}</sc-if>
3646              </span>
3647            </div>
3648          </div>
3649          <sc-if value="{{ heroIsAuction }}">          <div class="home-hero-main" role="region" aria-label="Remates destacados" tabindex="0" onPointerDown="{{ heroPointerDown }}" onPointerUp="{{ heroPointerUp }}" onPointerCancel="{{ heroPointerCancel }}" onKeyDown="{{ heroKeyDown }}" style="padding:clamp(28px, 5vw, 52px) clamp(16px, 4vw, 36px) 30px; color:#fff; display:flex; justify-content:space-between; align-items:flex-end; gap:40px; flex-wrap:wrap; margin-top:auto;">
3650            <div class="home-hero-copy" style="width:min(860px, 100%); max-width:860px; flex:1 1 680px; min-width:0;">
3651            <sc-if value="{{ homeHeroReady }}">
3652            <div class="home-hero-slide-copy" key="{{ heroTextKey }}
3652" style="animation:heroTextFade 1200ms ease both;">
3653              <sc-if value="{{ heroHasConsLogo }}" hint-placeholder-val="false">
3654                <div class="home-hero-brand" style="width:min(290px,72vw); height:clamp(48px,7vw,72px); margin-bottom:18px; display:flex; align-items:center; justify-content:flex-start; filter:drop-shadow(0 4px 18px rgba(0,0,0,.34));">
3655                  <img src="{{ heroConsLogo }}" alt="{{ heroConsName }}" style="display:block; width:auto; max-width:100%; max-height:100%; object-fit:contain; object-position:left center; filter:{{ heroConsLogoFilter }};">
3656                </div>
3657              </sc-if>
3658              <span class="home-hero-badge" style="display:inline-flex; align-items:center; gap:8px; background:{{ heroBadgeBg }}; font-size:12px; font-weight:700; padding:6px 14px; border-radius:100px; letter-spacing:0.08em;"><span class="home-hero-status-dot" aria-hidden="true" style="width:7px; height:7px; border-radius:50%; background:#fff; animation:pulseDot 1.4s infinite;"></span><span class="home-hero-status-text">{{ heroBadge }}</span></span>
3659              <sc-if value="{{ heroPrebidsActive }}" hint-placeholder-val="false"><button type="button" class="prebid-active-badge is-hero" onClick="{{ heroOpenLots }}"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></button></sc-if>
3660              <h1 class="home-hero-title" style="font-size:clamp(30px, 5vw, 50px); font-weight:800; letter-spacing:-0.025em; line-height:1.05; margin:16px 0 10px; text-wrap:balance; max-width:100%; overflow-wrap:anywhere;">{{ heroTitle }}</h1>
3661              <p class="home-hero-subtitle" style="margin:0; font-size:clamp(14px, 1.6vw, 16px); color:#c8d8ea; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; min-height:3em;">{{ heroSubtitle }}</p>
3662              <sc-if value="{{ heroPrebidsUpcoming }}" hint-placeholder-val="false"><section class="home-prebid-countdown"><span class="home-prebid-countdown-label">Las preofertas comienzan</span><strong class="home-prebid-countdown-date">{{ heroPrebidStartLabel }}</strong><prebid-start-countdown start="{{ heroPrebidStart }}"></prebid-start-countdown></section></sc-if>
3663              <div class="home-hero-actions" style="display:flex; gap:12px; margin-top:22px; flex-wrap:wrap;">
3664                <a href="{{ heroHref }}" target="_blank" rel="noopener" style="background:#fff; color:#002e5f; padding:13px 26px; border-radius:12px; font-weight:700; font-size:15px; transition:transform 160ms;" style-hover="transform:translateY(-2px); color:#002e5f;">Ver transmisión</a>
3665                <span onClick="{{ heroOpenLots }}" style="background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.25); padding:13px 26px; border-radius:12px; font-size:15px; cursor:pointer;" style-hover="background:rgba(255,255,255,0.22);">Ver Lotes</span>
3666              </div>
3667            </div>
3668            </sc-if>
3669              <sc-if value="{{ heroHasSlides }}" hint-placeholder-val="{{ false }}">
3670                <div class="home-hero-dots" style="display:flex; gap:8px; margin-top:24px; align-items:center;">
3671                  <sc-for list="{{ heroDots }}" as="d" hint-placeholder-count="5">
3672                    <span onClick="{{ d.go }}" style="height:6px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:width 300ms ease;"></span>
3673                  </sc-for>
3674                </div>
3675              </sc-if>
3676            </div>
3677            <div class="home-hero-indices" style="display:flex; gap:12px; flex-wrap:wrap;">
3678              <sc-if value="{{ homeIndicesReady }}">
3679              <sc-for list="{{ heroIndices }}" as="s" hint-placeholder-count="3">
3680                <div class="home-hero-index-card" style="position:relative; background:rgba(255,255,255,0.1); backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,0.2); border-radius:16px; padding:14px 18px; min-width:132px; overflow:hidden;">
3681                  <div style="display:flex; align-items:center; justify-content:space-between; gap:10px;">
3682                    <span style="font-size:10.5px; color:#c8d8ea; letter-spacing:0.06em; font-weight:700; text-transform:uppercase;">{{ s.label }}</span>
3683                    <span style="display:inline-flex; align-items:center; gap:2px; font-size:11.5px; font-weight:800; color:{{ s.color }}; background:rgba(255,255,255,0.08); padding:2px 7px 2px 4px; border-radius:100px;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="{{ s.color }}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0" data-dc-svg-d="{{ s.arrow }}"></path></svg>{{ s.pctFmt }}</span>
3684                  </div>
3685                  <div class="home-hero-index-value" style="font-size:clamp(20px, 2.6vw, 27px); font-weight:800; font-variant-numeric:tabular-nums; margin-top:6px;">{{ s.rollEl }}<span class="home-hero-index-unit" style="font-size:12px; font-weight:600; color:#9db4cc;">{{ s.unit }}</span></div>
3686                  <div class="home-hero-index-month" style="font-size:10px; color:#8fa6bf; margin-top:1px;">vs. anterior · {{ s.mes }}</div>
3687                </div>
3688              </sc-for>
3689              </sc-if>
3690              <sc-if value="{{ homeIndicesEmpty }}">
3691                <div style="background:rgba(255,255,255,.09); border:1px solid rgba(255,255,255,.16); border-radius:16px; padding:15px 18px; color:#c8d8ea; font-size:12px; min-width:210px;">Índices temporalmente no disponibles</div>
3692              </sc-if>
3693            </div>
3694          </div>
3695</sc-if>
3696          <sc-if value="{{ heroIsCongress }}"><section class="home-congress-banner" aria-label="5° Congreso Federal Ganadero" tabindex="0" onPointerDown="{{ heroPointerDown }}" onPointerUp="{{ heroPointerUp }}" onPointerCancel="{{ heroPointerCancel }}" onKeyDown="{{ heroKeyDown }}">
3697            <div class="home-congress-copy">
3698              <img class="home-congress-logo" src="assets/congreso-2026/logo.webp" width="630" height="96" alt="Rosgan · 5° Congreso Federal Ganadero">
3699              <h1>Quo vadis,<br><span>ganadería</span></h1>
3700              <p class="home-congress-lead">Una jornada para definir el futuro de la ganadería argentina.</p>
3701              <p class="home-congress-date"><strong>12 NOVIEMBRE 2026</strong><span>Desde las 8:30 h · Bolsa de Comercio de Rosario</span></p>
3702              <div class="home-congress-actions"><a class="home-congress-register" href="https://inscripcion.ar/CFG2026/" target="_blank" rel="noopener">Inscribite aquí <img src="assets/icons/lucide/arrow-up-right.svg" alt="" aria-hidden="true" width="16" height="16"></a><a class="home-congress-program" href="https://congresofederalganadero.com.ar/" target="_blank" rel="noopener">Conocé el Congreso <img src="assets/icons/lucide/arrow-up-right.svg" alt="" aria-hidden="true" width="16" height="16"></a></div>
3703              <small class="home-congress-note">Inscripción gratuita · Cupos limitados</small>
3704              <sc-if value="{{ heroHasSlides }}"><div class="home-hero-dots congress-dots"><sc-for list="{{ heroDots }}" as="d"><button type="button" onClick="{{ d.go }}" aria-label="{{ d.label }}" aria-pressed="{{ d.active }}" style="width:{{ d.w }};background:{{ d.bg }};"></button></sc-for></div></sc-if>
3705            </div>
3706            <div class="home-congress-visual"><img src="assets/congreso-2026/hero-bovino.webp" width="720" height="677" fetchpriority="high" alt="Bovino sobre círculo verde, imagen del Congreso Federal Ganadero"></div>
3707          </section></sc-if>
3708        </div>
3709      </div>
3710      <style>
3711        .home-access{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;padding:0 clamp(16px,4vw,36px);position:relative}
3712        .home-access-card{appearance:none;width:100%;min-width:0;display:flex;align-items:center;gap:14px;padding:22px 20px;border:1px solid #e0e8f0;border-radius:18px;background:#fff;color:#112c46;text-align:left;font:inherit;cursor:pointer;box-shadow:0 8px 24px rgba(10,40,70,.065);transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease}
3713        .home-access-icon{width:48px;height:48px;flex:none;display:grid;place-items:center;border-radius:15px;background:#f1f5f9;color:#143d62;border:1px solid #e5ecf3}
3714        .home-access-icon img{display:block;width:40px;height:40px;object-fit:contain}
3715        .home-access-copy{display:flex;flex:1;min-width:0;flex-direction:column;gap:5px}
3716        .home-access-copy strong{font-size:17px;font-weight:750;letter-spacing:-.025em;line-height:1.2}
3717        .home-access-copy small{font-size:12px;line-height:1.45;color:#64788b;text-wrap:balance}
3718        .home-access-arrow{width:17px;height:17px;flex:none;color:#91a2b3;transition:transform .2s ease,color .2s ease}
3719        .home-access-card:focus-visible{outline:3px solid #2079c5;outline-offset:4px}
3720        @media(hover:hover){.home-access-card:hover{transform:translateY(-3px);border-color:var(--access-color);box-shadow:0 12px 28px rgba(10,40,70,.1)}.home-access-card:hover .home-access-arrow{color:var(--access-color);transform:translateX(3px)}}
3721        .home-access-card:active{transform:translateY(0);box-shadow:0 3px 12px rgba(10,40,70,.06)}
3722        @media(max-width:1100px){.home-access{grid-template-columns:repeat(2,minmax(0,1fr))}}
3723        @media(max-width:540px){.home-access{gap:10px}.home-access-card{padding:16px 13px;gap:10px;align-items:flex-start;flex-wrap:wrap}.home-access-icon{width:38px;height:38px;border-radius:12px}.home-access-icon img{width:32px;height:32px}.home-access-copy{flex-basis:calc(100% - 48px)}.home-access-copy strong{font-size:15px}.home-access-copy small{font-size:11px;text-wrap:initial}.home-access-arrow{display:none}}
3724        @media(prefers-reduced-motion:reduce){.home-access-card,.home-access-arrow{transition:none}}
3725      </style>
3726      <div class="home-access" aria-label="Explorar Rosgan" style="margin-top:{{ accessPull }};">
3727        <sc-for list="{{ accessCards }}" as="c" hint-placeholder-count="4">
3728          <button type="button" class="home-access-card" onClick="{{ c.go }}" style="--access-color:{{ c.chipBg }};--access-tint:{{ c.tint }};">
3729            <span class="home-access-icon" aria-hidden="true"><img data-dc-attr-src="{{ c.iconImage }}" alt="" width="40" height="40"></span>
3730            <span class="home-access-copy"><strong>{{ c.title }}</strong><small>{{ c.text }}</small></span>
3731            <svg class="home-access-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"></path></svg>
3732          </button>
3733        </sc-for>
3734      </div>
3735      <section class="home-channel" aria-label="Canal de Rosgan">
3736        <header><div><small>ROSGAN TV</small><h2>El canal de hoy</h2></div><sc-if value="{{ hcManual }}"><button type="button" onClick="{{ hcAutomatic }}">Volver a programación automática</button></sc-if></header>
3737        <div class="home-channel-layout">
3738          <div class="home-channel-screen" id="home-channel-player">
3739            <sc-if value="{{ hcHasYoutube }}"><iframe key="{{ hcPlayerKey }}" src="{{ hcEmbed }}" title="{{ hcTitle }}" loading="eager" allow="autoplay; encrypted-media; picture-in-picture; fullscreen" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></sc-if>
3740            <sc-if value="{{ hcIsSpot }}"><video key="{{ hcPlayerKey }}" src="{{ hcSpotUrl }}" data-home-spot-player="true" data-spot-loop="{{ hcSpotLoop }}" autoplay muted playsinline controls preload="auto" onEnded="{{ hcSpotEnded }}" onError="{{ hcSpotError }}" aria-label="{{ hcTitle }}"></video></sc-if>
3741            <sc-if value="{{ hcEmpty }}"><div class="home-channel-empty"><img src="assets/rosgan-logo-blue.png" alt="Rosgan"><h3>{{ hcEmptyTitle }}</h3><p>Cuando haya programación, la transmisión aparecerá acá automáticamente.</p></div></sc-if>
3742          </div>
3743          <aside><div class="home-channel-current"><span class="home-channel-badge {{ hcLiveClass }}">{{ hcBadge }}</span><h3>{{ hcTitle }}</h3><p>{{ hcReason }}</p><sc-if value="{{ hcHasVideo }}"><small>Reproducción automática silenciada. Activá el sonido desde el reproductor.</small><sc-if value="{{ hcHasYoutube }}"><div class="home-channel-links"><a class="home-channel-youtube" href="{{ hcHref }}" target="_blank" rel="noopener" aria-label="Ver en YouTube (abre en otra pestaña)"><svg width="25" height="18" viewBox="0 0 28 20" aria-hidden="true"><rect width="28" height="20" rx="6" fill="#ff0033"/><path d="M11 5.5 19 10l-8 4.5Z" fill="white"/></svg><span>Ver en YouTube</span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M10 14 21 3M21 14v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6"/></svg></a></div></sc-if></sc-if></div>
3744            <sc-if value="{{ hcHasToday }}"><div class="home-channel-today home-channel-picker"><div class="home-channel-picker-heading"><strong>Canales de hoy</strong></div><nav class="home-channel-programs" aria-label="Canales de hoy"><sc-for list="{{ hcToday }}" as="program"><button class="home-channel-chip" type="button" onClick="{{ program.select }}" aria-label="{{ program.title }}" title="{{ program.title }}" aria-pressed="{{ program.selected }}"><img src="{{ program.thumb }}" alt=""><span><strong>{{ program.label }}</strong><small>{{ program.time }}</small></span></button></sc-for></nav></div></sc-if>
3745            <sc-if value="{{ hcHasSpots }}"><div class="home-channel-today home-channel-picker"><div class="home-channel-picker-heading"><strong>Spots institucionales</strong></div><nav aria-label="Spots institucionales"><sc-for list="{{ hcSpots }}" as="spot"><button class="home-channel-chip" type="button" onClick="{{ spot.select }}" aria-pressed="{{ spot.selected }}"><img src="assets/rosgan-logo-blue.png" alt="" style="object-fit:contain;background:#fff;padding:8px;box-sizing:border-box;"><span><strong>{{ spot.title }}</strong><small>▶ Ver spot · reproducción continua</small></span></button></sc-for></nav></div></sc-if>
3746          </aside>
3747        </div>
3748      </section>
3749      <sc-if value="{{ homeMonthLoading }}">
3750        <div aria-label="Cargando próximos remates" style="padding:28px clamp(16px, 4vw, 36px) 4px;">
3751          <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:126px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:270px; max-width:72vw; height:25px; border-radius:8px;"></span></div>
3752          <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(260px,30vw,360px); gap:16px; overflow:hidden; padding-bottom:14px;">
3753            <sc-for list="{{ homeSectionSkeletons }}" as="sk" hint-placeholder-count="4"><div class="home-loader-card"><div class="home-skeleton" style="aspect-ratio:16/9;"></div><div style="display:flex; justify-content:space-between; padding:12px 15px; gap:18px;"><span class="home-skeleton" style="width:62%; height:13px; border-radius:6px;"></span><span class="home-skeleton" style="width:25%; height:13px; border-radius:6px;"></span></div></div></sc-for>
3754          </div>
3755        </div>
3756      </sc-if>
3757      <sc-if value="{{ hasMonthCarousel }}" hint-placeholder-val="{{ false }}">
3758        <div style="padding:28px clamp(16px, 4vw, 36px) 4px;">
3759          <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px;">
3760            <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#e63946; font-weight:700;">AGENDA DE REMATES</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:2px 0 0; color:#112033;">Próximos Remates</h2></div>
3761          <span onClick="{{ goRemates }}" style="display:inline-flex;align-items:center;gap:5px;font-size:13.5px; color:#0a5ca8; font-weight:600; cursor:pointer;">Ver archivo <img src="assets/icons/lucide/arrow-right.svg" alt="" style="width:14px;height:14px;"></span>
3762          </div>
3763          <div data-drag-scroll style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(260px, 30vw, 360px); gap:16px; overflow-x:auto; padding-bottom:14px; cursor:grab; scrollbar-width:none; -ms-overflow-style:none; scroll-snap-type:x mandatory;">
3764            <sc-for list="{{ monthCarousel }}" as="m" hint-placeholder-count="4">
3765              <a class="home-agenda-flyer" onClick="{{ m.select }}" href="{{ m.href }}" target="_blank" rel="noopener" data-drag-item style="--flyer-brand:{{ m.brandColor }};">
3766                <div class="home-month-card-media" style="position:relative; height:clamp(180px,17vw,220px); border-top:4px solid {{ m.brandColor }}; box-sizing:border-box;">
3767                  <div data-drag-img style="position:absolute; inset:0; background-image:url('{{ m.thumb }}'); background-size:cover; background-position:{{ m.thumbPosition }}; opacity:.42; filter:saturate(.68) contrast(1.06); transition:transform 600ms cubic-bezier(0.22,0.72,0.18,1);"></div>
3768                  <div style="position:absolute; inset:0; background:{{ m.thumbOverlay }};"></div>
3769                  <span style="position:absolute; top:12px; left:12px; background:{{ m.badgeBg }}; color:{{ m.badgeColor }}; font-size:10px; font-weight:850; padding:5px 10px; border-radius:100px; letter-spacing:.07em; display:inline-flex; align-items:center; gap:6px; box-shadow:0 4px 14px rgba(0,0,0,.14);"><sc-if value="{{ m.live }}" hint-placeholder-val="{{ false }}"><span style="position:relative; width:7px; height:7px;"><span style="position:absolute; inset:0; border-radius:50%; background:#fff;"></span><span style="position:absolute; inset:0; border-radius:50%; background:#fff; animation:livePing 1.6s ease-out infinite;"></span></span></sc-if>{{ m.badge }}</span>
3770                  <div class="home-month-card-logos" style="position:absolute; left:50%; top:32%; transform:translate(-50%,-50%); width:min(76%,244px); height:38px; display:flex; align-items:center; justify-content:center; gap:14px; pointer-events:none; filter:drop-shadow(0 4px 14px rgba(0,0,0,.3));">
3771                    <sc-if value="{{ m.hasConsLogo }}" hint-placeholder-val="false"><img src="{{ m.consLogo }}" alt="" style="width:48px; height:48px; flex:none; object-fit:contain; filter:{{ m.consLogoFilter }};"></sc-if>
3772                    <strong class="home-flyer-brand-name">{{ m.cons }}</strong>
3773                  </div>
3774                </div>
3775                <div class="home-flyer-body">
3776                  <div class="home-flyer-schedule"><span class="home-flyer-date"><strong>{{ m.day }}</strong><span>{{ m.month }}</span></span><span><strong>{{ m.time }}</strong><span>{{ m.location }}</span></span></div>
3777                  <strong class="home-flyer-title">{{ m.title }}</strong>
3778                  <span class="home-flyer-cta">{{ m.cta }} <img src="assets/icons/lucide/arrow-right.svg" alt=""></span>
3779                </div>
3780              </a>
3781            </sc-for>
3782          </div>
3783          <sc-if value="{{ monthHasDots }}" hint-placeholder-val="{{ false }}">
3784            <div style="display:flex; gap:8px; align-items:center; justify-content:center; margin-top:12px;">
3785              <sc-for list="{{ monthDots }}" as="d" hint-placeholder-count="4">
3786                <span onClick="{{ d.go }}" style="height:6px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:all 300ms;"></span>
3787              </sc-for>
3788            </div>
3789          </sc-if>
3790        </div>
3791      </sc-if>
3792      <sc-if value="{{ homeCafeVideosLoading }}">
3793        <div aria-label="Cargando videos de Café Ganadero" style="padding:22px clamp(16px, 4vw, 36px) 8px;">
3794          <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:110px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:280px; max-width:78vw; height:25px; border-radius:8px;"></span></div>
3795          <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(240px,26vw,320px); gap:16px; overflow:hidden; padding-bottom:14px;"><sc-for list="{{ homeSectionSkeletons }}" as="sk" hint-placeholder-count="4"><div class="home-loader-card"><div class="home-skeleton" style="aspect-ratio:16/9;"></div><div style="padding:13px 14px; display:grid; gap:9px;"><span class="home-skeleton" style="width:84%; height:14px; border-radius:7px;"></span><span class="home-skeleton" style="width:46%; height:11px; border-radius:6px;"></span></div></div></sc-for></div>
3796        </div>
3797      </sc-if>
3798      <sc-if value="{{ hasCafeVideos }}" hint-placeholder-val="{{ false }}">
3799        <div style="padding:22px clamp(16px, 4vw, 36px) 8px;">
3800          <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px;">
3801          <div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:0; color:#112033;">Café Ganadero · Videos recientes</h2><p style="font-size:14px; line-height:1.5; color:#65758a; margin:6px 0 0;">{{ cafeText }}</p></div>
3802          </div>
3803          <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(240px, 26vw, 320px); gap:16px; overflow-x:auto; padding-bottom:14px; scroll-snap-type:x mandatory;">
3804            <sc-for list="{{ cafeCards }}" as="c" hint-placeholder-count="4">
3805              <a href="{{ c.href }}" onClick="{{ c.select }}" target="_blank" rel="noopener" data-reveal style="scroll-snap-align:start; background:#fff; border-radius:16px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); color:#112033; display:block; transition:transform 220ms;" style-hover="transform:translateY(-4px); color:#112033;">
3806                <div style="aspect-ratio:16/9; background-image:url('{{ c.thumb }}'); background-size:cover; background-position:center; position:relative;">
3807                <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:44px; height:44px; border-radius:50%; background:rgba(230,57,70,0.9); display:grid; place-items:center; color:#fff; font-size:15px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span>
3808                </div>
3809                <div style="padding:12px 14px;"><strong style="font-size:14px; line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ c.title }}</strong><span style="font-size:12px; color:#65758a;">{{ c.meta }}</span></div>
3810              </a>
3811            </sc-for>
3812          </div>
3813        </div>
3814      </sc-if>
3815      <sc-if value="{{ hasCafePlaylistFallback }}" hint-placeholder-val="{{ false }}">
3816        <div style="padding:22px clamp(16px, 4vw, 36px) 8px;">
3817          <div style="display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline; margin-bottom:16px; gap:14px;">
3818            <div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:0; color:#112033;">Café Ganadero · Últimos programas</h2><p style="font-size:14px; line-height:1.5; color:#65758a; margin:6px 0 0;">{{ cafeText }}</p></div>
3819            <a href="{{ cafePlaylistHref }}" target="_blank" rel="noopener" style="color:#005ca8; font-size:13px; font-weight:750; white-space:nowrap;">Ver en YouTube →</a>
3820          </div>
3821          <div data-reveal style="display:grid; grid-template-columns:{{ cafePlaylistGrid }}; background:#fff; border:1px solid #dce7f2; border-radius:20px; overflow:hidden; box-shadow:0 12px 34px rgba(0,30,60,.09);">
3822            <iframe src="{{ cafePlaylistEmbed }}" title="Playlist oficial de Café Ganadero" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="width:100%; aspect-ratio:16/9; border:0; background:#001d3d;"></iframe>
3823            <div style="padding:clamp(20px, 3vw, 32px); display:flex; flex-direction:column; justify-content:center; gap:10px;">
3824              <span style="width:42px; height:42px; border-radius:13px; display:grid; place-items:center; background:#e63946; color:#fff;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span>
3825              <strong style="font-size:clamp(18px, 2vw, 23px); line-height:1.15; color:#112033;">Todos los programas, en un solo lugar</strong>
3826              <span style="font-size:14px; line-height:1.55; color:#65758a;">Reproducí los últimos episodios de Café Ganadero directamente desde la playlist oficial de Rosgan.</span>
3827            </div>
3828          </div>
3829        </div>
3830      </sc-if>
3831      <sc-if value="{{ homeLoteNewsLoading }}">
3832        <div aria-label="Cargando Lote de Noticias" style="padding:8px clamp(16px, 4vw, 36px) 30px;">
3833          <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:125px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:180px; height:25px; border-radius:8px;"></span></div>
3834          <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(230px,30vw,300px); gap:14px; overflow:hidden; padding-bottom:14px;"><sc-for list="{{ homeSectionSkeletons }}" as="sk" hint-placeholder-count="4"><div class="home-skeleton" style="min-height:190px; border-radius:18px;"></div></sc-for></div>
3835        </div>
3836      </sc-if>
3837      <sc-if value="{{ hasLoteNoticias }}" hint-placeholder-val="{{ false }}">
3838      <div style="padding:8px clamp(16px, 4vw, 36px) 30px;">
3839        <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px; gap:12px;">
3840          <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#e63946; font-weight:700;">INFORME SEMANAL</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:800; margin:2px 0 0; color:#112033;">Lote de Noticias</h2></div>
3841        </div>
3842        <div class="home-report-track">
3843          <sc-for list="{{ loteCards }}" as="l" hint-placeholder-count="4">
3844            <a class="home-report-card" href="{{ l.href }}" target="{{ l.target }}" rel="noopener">
3845              <div class="home-report-meta"><span class="home-report-icon"><img src="assets/icons/lucide/file-text.svg" alt=""></span><span>{{ l.date }}</span></div>
3846              <sc-if value="{{ l.featured }}" hint-placeholder-val="false"><span class="home-report-latest">Última edición</span></sc-if>
3847              <h3>{{ l.title }}</h3>
3848              <div class="home-report-footer"><span>Leer informe</span><span class="home-report-arrow"><img src="assets/icons/lucide/arrow-up-right.svg" alt=""></span></div>
3849            </a>
3850          </sc-for>
3851        </div>
3852      </div>
3853      </sc-if>
3854
3855      <sc-if value="{{ homeNewsLoading }}">
3856      <div aria-label="Cargando noticias" style="padding:8px clamp(16px, 4vw, 36px) 34px;">
3857        <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:135px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:105px; height:25px; border-radius:8px;"></span></div>
3858        <div style="display:grid; grid-template-columns:{{ noticiasGrid }}; gap:16px;">
3859          <div class="home-loader-card"><div class="home-skeleton" style="min-height:clamp(280px,34vw,380px);"></div></div>
3860          <div class="home-loader-card"><div class="home-skeleton" style="aspect-ratio:16/9;"></div><div style="padding:16px; display:grid; gap:10px;"><span class="home-skeleton" style="width:35%; height:11px; border-radius:6px;"></span><span class="home-skeleton" style="width:88%; height:17px; border-radius:7px;"></span><span class="home-skeleton" style="width:72%; height:13px; border-radius:6px;"></span></div></div>
3861        </div>
3862      </div>
3863      </sc-if>
3864      <sc-if value="{{ hasNoticias }}" hint-placeholder-val="{{ false }}">
3865      <div style="padding:8px clamp(16px, 4vw, 36px) 34px;">
3866        <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px; gap:12px;">
3867          <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#8593a6; font-weight:600;">ACTUALIDAD ROSGAN</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:800; margin:2px 0 0; color:#112033;">Noticias</h2></div>
3868        </div>
3869        <div style="display:grid; grid-template-columns:{{ noticiasGrid }}; gap:16px;">
3870          <a href="{{ noticiaLead.href }}" target="_blank" rel="noopener" data-reveal onMouseEnter="{{ leadHoverIn }}" onMouseLeave="{{ leadHoverOut }}" style="position:relative; display:flex; flex-direction:column; justify-content:flex-end; min-height:clamp(280px, 34vw, 380px); border-radius:20px; overflow:hidden; color:#fff; box-shadow:0 12px 34px rgba(0,30,60,0.16); transition:transform 240ms;" style-hover="transform:translateY(-4px); color:#fff;">
3871            <div data-lead-img style="position:absolute; inset:0; background-image:url('{{ noticiaLead.hero }}'); background-size:cover; background-position:center; transition:transform 500ms;"></div>
3872            <div style="position:absolute; inset:0; background:linear-gradient(to top, rgba(3,18,38,0.92) 8%, rgba(3,18,38,0.35) 55%, rgba(3,18,38,0.1));"></div>
3873            <div style="position:relative; padding:clamp(20px, 3vw, 30px);">
3874              <span style="display:inline-block; background:#e63946; font-size:10.5px; font-weight:800; letter-spacing:0.07em; padding:5px 12px; border-radius:100px; text-transform:uppercase; margin-bottom:12px;">{{ noticiaLead.category }}</span>
3875              <h3 style="font-size:clamp(20px, 2.6vw, 27px); font-weight:800; line-height:1.15; margin:0 0 10px; text-wrap:balance; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;">{{ noticiaLead.title }}</h3>
3876              <p style="margin:0 0 12px; font-size:14px; line-height:1.5; color:#d6e2f0; max-width:640px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ noticiaLead.excerpt }}</p>
3877              <span style="font-size:12px; font-weight:600; color:#a9c3de;">{{ noticiaLead.date }}</span>
3878            </div>
3879          </a>
3880          <div style="display:flex; flex-direction:column; gap:12px;">
3881            <a href="{{ notiCurrent.href }}" target="_blank" rel="noopener" key="{{ notiKey }}" style="flex:1; display:flex; flex-direction:column; background:#fff; border:1px solid #eef3f9; border-radius:16px; overflow:hidden; color:#112033; box-shadow:0 8px 26px rgba(0,30,60,0.08); animation:heroTextFade 700ms ease both; transition:transform 220ms, box-shadow 220ms;" style-hover="transform:translateY(-4px); box-shadow:0 16px 38px rgba(0,30,60,0.15); color:#112033;">
3882              <div style="aspect-ratio:16/9; background-image:url('{{ notiCurrent.hero }}'); background-size:cover; background-position:center;"></div>
3883              <div style="padding:15px 16px; display:flex; flex-direction:column; flex:1;">
3884                <span style="font-size:9.5px; font-weight:800; letter-spacing:0.06em; color:#e63946; text-transform:uppercase; display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden;">{{ notiCurrent.category }}</span>
3885                <strong style="font-size:16px; line-height:1.28; margin:6px 0 8px; text-wrap:pretty; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ notiCurrent.title }}</strong>
3886                <p style="margin:0 0 10px; font-size:13px; line-height:1.5; color:#65758a; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;">{{ notiCurrent.excerpt }}</p>
3887                <span style="font-size:11.5px; color:#8593a6; margin-top:auto;">{{ notiCurrent.date }}</span>
3888              </div>
3889            </a>
3890            <sc-if value="{{ hasNotiDots }}" hint-placeholder-val="{{ false }}">
3891              <div style="display:flex; gap:8px; justify-content:center; align-items:center; padding:2px 0;">
3892                <sc-for list="{{ notiDots }}" as="d" hint-placeholder-count="4">
3893                  <span onClick="{{ d.go }}" style="height:7px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:width 300ms ease, background 300ms ease;"></span>
3894                </sc-for>
3895              </div>
3896            </sc-if>
3897          </div>
3898        </div>
3899      </div>
3900      </sc-if>
3901
3902      <div style="padding:0 clamp(16px, 4vw, 36px) 40px;">
3903        <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px;">
3904          <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#8593a6; font-weight:600;">CONSIGNATARIAS</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:2px 0 0; color:#112033;">Sitios web oficiales</h2></div>
3905        </div>
3906        <sc-if value="{{ homeChannelsLoading }}">
3907          <div aria-label="Cargando consignatarias" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:12px;">
3908            <sc-for list="{{ homeChannelSkeletons }}" as="sk" hint-placeholder-count="6"><div class="home-loader-card" style="padding:14px 16px; display:flex; align-items:center; gap:12px;"><span class="home-skeleton" style="width:44px; height:44px; border-radius:12px; flex:none;"></span><span style="display:grid; gap:8px; flex:1;"><span class="home-skeleton" style="width:78%; height:14px; border-radius:6px;"></span><span class="home-skeleton" style="width:48%; height:11px; border-radius:6px;"></span></span></div></sc-for>
3909          </div>
3910        </sc-if>
3911        <sc-if value="{{ homeChannelsReady }}">
3912        <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(210px, 1fr)); gap:12px;">
3913          <sc-for list="{{ channelCards }}" as="ch" hint-placeholder-count="6">
3914            <a data-reveal data-dc-attr-href="{{ ch.href }}" target="_blank" rel="noopener noreferrer" style="text-decoration:none; background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 6px 20px rgba(0,30,60,0.07); display:flex; align-items:center; gap:12px; cursor:pointer; transition:transform 200ms; border:1px solid #eef3f9;" style-hover="transform:translateY(-3px); box-shadow:0 14px 34px rgba(0,30,60,0.13); border-color:#dde7f2;">
3915              <span style="width:44px; height:44px; border-radius:12px; background:{{ ch.chipBg }}; color:#fff; display:grid; place-items:center; font-size:14px; font-weight:800; flex-shrink:0; overflow:hidden;">
3916                <sc-if value="{{ ch.hasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ ch.logo }}" alt="" aria-hidden="true" style="display:block; width:34px; height:34px; object-fit:contain; filter:brightness(0) invert(1);"></sc-if>
3917                <sc-if value="{{ ch.noLogo }}" hint-placeholder-val="{{ true }}">{{ ch.initial }}</sc-if>
3918              </span>
3919              <div style="min-width:0;"><strong style="font-size:14px; color:#112033; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ ch.name }}</strong><span style="font-size:12px; color:#65758a;">Sitio oficial</span></div>
3920            </a>
3921          </sc-for>
3922        </div>
3923        </sc-if>
3924      </div>
3925      </div>
3926    </sc-if>
3927
3928    <!-- ============ MERCADO · ESTADÍSTICAS ============ -->
3929    <sc-if value="{{ showStatistics }}" hint-placeholder-val="{{ false }}"><rosgan-historical-statistics></rosgan-historical-statistics><rosgan-statistics-dashboard></rosgan-statistics-dashboard></sc-if>
3930
3931    <!-- ============ POOL ROSLOT ============ -->
3932    <sc-if value="{{ isPool }}" hint-placeholder-val="{{ false }}">
3933      <div class="roslot-pool-page" style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px);">
3934        <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; padding:26px 0 18px;">
3935          <div>
3936          <div style="font-size:11.5px; letter-spacing:0.14em; color:#e63946; font-weight:700;">ROSLOT · MERCADO DIGITAL</div>
3937            <h1 style="font-size:clamp(24px, 3.4vw, 34px); font-weight:800; letter-spacing:-0.02em; margin:4px 0 0; color:#112033;">Buscador de Lotes</h1>
3938            <div class="roslot-presence-strip" role="status" aria-live="polite" title="Usuarios autenticados con actividad durante los últimos minutos">
3939              <span class="roslot-presence-live"><i></i>Mirando ahora</span>
3940              <span class="roslot-presence-role"><span class="roslot-presence-role-icon"><i class="dc-lucide dc-lucide-shopping-cart" aria-hidden="true"></i></span><span class="roslot-presence-role-copy"><strong>{{ roslotPresenceBuyers }}</strong><small>{{ roslotPresenceBuyerLabel }}</small></span></span>
3941              <span class="roslot-presence-role is-seller"><span class="roslot-presence-role-icon"><i class="dc-lucide dc-lucide-store" aria-hidden="true"></i></span><span class="roslot-presence-role-copy"><strong>{{ roslotPresenceSellers }}</strong><small>{{ roslotPresenceSellerLabel }}</small></span></span>
3942            </div>
3943            <div style="width:min(100%,360px); margin-top:9px;">
3944              <div style="display:flex; align-items:center; gap:9px; flex-wrap:wrap;">
3945                <span style="display:inline-flex; align-items:center; gap:7px; padding:6px 10px; border-radius:100px; background:{{ marketStatusBg }}; color:{{ marketStatusColor }}; border:1px solid {{ marketStatusBorder }}; font-size:11.5px; font-weight:800; letter-spacing:0.02em;">
3946                  <img data-dc-attr-src="{{ marketStatusIcon }}" alt="" aria-hidden="true" width="14" height="14" style="width:14px; height:14px;">
3947                  {{ marketStatusLabel }}
3948                </span>
3949                <strong data-roslot-market-countdown style="font-size:11.5px; color:{{ marketTimerColor }};">{{ marketCountdownLabel }}</strong>
3950              </div>
3951              <div data-roslot-market-progress role="progressbar" aria-label="Tiempo hasta el próximo cambio del mercado" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ marketProgressValue }}" style="height:6px; margin-top:8px; overflow:hidden; border-radius:100px; background:#dfe7f0; box-shadow:inset 0 1px 2px rgba(17,32,51,.08);">
3952                <span data-roslot-market-progress-fill style="display:block; width:{{ marketProgress }}; height:100%; border-radius:inherit; background:{{ marketProgressBg }}; transition:width 1s linear;"></span>
3953              </div>
3954              <small data-roslot-market-schedule style="display:block; margin-top:5px; color:#687a8f; font-size:10.5px; font-weight:650;">{{ marketScheduleLabel }}</small>
3955            </div>
3956          </div>
3957          <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
3958            <button class="roslot-report-entry" onClick="{{ goRoslotSummary }}"><i class="dc-lucide dc-lucide-receipt-text" aria-hidden="true"></i>Resumen del día</button>
3959            <sc-if value="{{ canLoadLot }}" hint-placeholder-val="{{ false }}"><button onClick="{{ goLoad }}" style="display:inline-flex; align-items:center; gap:7px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:9px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer; transition:transform 160ms;" style-hover="transform:translateY(-2px);"><span style="font-size:15px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button></sc-if>
3960            <sc-for list="{{ statusTabs }}" as="t" hint-placeholder-count="2">
3961              <span onClick="{{ t.go }}" style="display:inline-flex; align-items:center; gap:8px; padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:{{ t.color }}; transition:transform 160ms;" style-hover="transform:translateY(-2px);">
3962                <sc-if value="{{ t.live }}" hint-placeholder-val="{{ false }}"><span style="position:relative; width:7px; height:7px;"><span style="position:absolute; inset:0; border-radius:50%; background:#e63946;"></span><span style="position:absolute; inset:0; border-radius:50%; background:#e63946; animation:livePing 1.6s ease-out infinite;"></span></span></sc-if>
3963                {{ t.label }}<strong style="font-variant-numeric:tabular-nums;">{{ t.count }}</strong>
3964              </span>
3965            </sc-for>
3966          </div>
3967        </div>
3968        <sc-if value="{{ showRoslotOperations }}" hint-placeholder-val="true">
3969          <button class="roslot-mobile-dock {{ roslotMobileDockClass }}" onClick="{{ openRoslotMobilePanel }}" aria-label="Abrir mis operaciones y actividad"><img src="assets/icons/lucide/radio-tower.svg" alt="">Actividad</button>
3970          <section class="roslot-ops-shell {{ roslotMobilePanelClass }}" aria-label="Panel de operaciones personales en vivo">
3971            <div class="roslot-mobile-panel-head"><strong>Mi panel Roslot</strong><button onClick="{{ closeRoslotMobilePanel }}" aria-label="Cerrar panel"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div>
3972            <article class="roslot-ops-panel">
3973              <div class="roslot-ops-head">
3974                <div class="roslot-ops-title"><small>PANEL PERSONAL</small><h2>Mis operaciones</h2></div>
3975                <button type="button" class="roslot-whatsapp-control {{ roslotWhatsappToggleClass }}" onClick="{{ toggleRoslotWhatsapp }}" disabled="{{ roslotWhatsappBusy }}" role="switch" aria-checked="{{ roslotWhatsappAriaChecked }}" title="{{ roslotWhatsappTitle }}">
3976                  <span class="roslot-whatsapp-copy"><span><img src="assets/icons/brands/whatsapp.svg" alt="" aria-hidden="true"></span><strong>Notificaciones por WhatsApp</strong><small>{{ roslotWhatsappStatus }}</small></span>
3977                  <span class="roslot-whatsapp-switch" aria-hidden="true"><i></i></span>
3978                </button>
3979              </div>
3980              <sc-if value="{{ roslotHasOperations }}" hint-placeholder-val="true"><div class="roslot-op-list"><sc-for list="{{ roslotOperations }}" as="operation" hint-placeholder-count="3">
3981                <div class="roslot-op-row {{ operation.rowClass }}">
3982                  <div class="roslot-op-copy">
3983                    <div class="roslot-op-topline"><span class="roslot-op-kind" style="background:{{ operation.kindBg }}; color:{{ operation.kindColor }};">{{ operation.kind }}</span><strong>{{ operation.title }}</strong><sc-if value="{{ operation.isOutbid }}" hint-placeholder-val="false"><span class="roslot-op-alert"><strong>TE SUPERARON</strong><span>Volvé a ofertar</span></span></sc-if></div>
3984                    <div class="roslot-op-meta-line"><p>{{ operation.meta }}</p><span class="roslot-op-status" style="color:{{ operation.statusColor }};">{{ operation.status }}</span></div>
3985                  </div>
3986                  <sc-if value="{{ operation.showPrice }}" hint-placeholder-val="false"><div class="roslot-op-price"><span>{{ operation.priceLabel }}</span><strong>{{ operation.priceValue }}</strong><em>{{ operation.priceDetail }}</em></div></sc-if>
3987                  <div class="roslot-op-actions">
3988                    <sc-if value="{{ operation.canBid }}" hint-placeholder-val="false"><button onClick="{{ operation.bid20 }}" disabled="{{ operation.disabled }}">+$20</button><button class="is-primary" onClick="{{ operation.bid50 }}" disabled="{{ operation.disabled }}">+$50</button></sc-if>
3989                    <sc-if value="{{ operation.canClose }}" hint-placeholder-val="false"><button class="is-hammer" onClick="{{ operation.close }}" disabled="{{ operation.disabled }}"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:14px;
3989height:14px;filter:brightness(0) invert(1);">{{ operation.closeLabel }}</button></sc-if>
3990                    <sc-if value="{{ operation.canApprove }}" hint-placeholder-val="false"><button class="is-danger" onClick="{{ operation.reject }}" disabled="{{ operation.disabled }}">Rechazar</button><button class="is-success" onClick="{{ operation.approve }}" disabled="{{ operation.disabled }}">Aprobar</button></sc-if>
3991                    <button onClick="{{ operation.open }}">Ver</button>
3992                  </div>
3993                  <sc-if value="{{ operation.bidSuccess }}" hint-placeholder-val="false"><div class="roslot-op-success" role="status" aria-live="assertive" aria-label="Oferta confirmada"><dotlottie-wc class="roslot-op-success-lottie" src="assets/auth/done.lottie" autoplay speed="1.45" aria-hidden="true"></dotlottie-wc><span class="roslot-op-success-copy"><small>OFERTA CONFIRMADA</small><strong>{{ operation.bidSuccessAmount }}</strong><span>Lote {{ operation.cert }}</span></span></div></sc-if>
3994                </div>
3995              </sc-for></div></sc-if>
3996              <sc-if value="{{ roslotOperationsEmpty }}" hint-placeholder-val="false"><div class="roslot-ops-empty">No tenés operaciones activas. Las ofertas y lotes propios aparecerán acá en tiempo real.</div></sc-if>
3997              <div class="roslot-day-summary">
3998                <div class="roslot-day-summary-head"><strong>Hacienda del día por categoría</strong><span>{{ roslotDailyCategoriesLabel }}</span></div>
3999                <sc-if value="{{ roslotHasDailyCategories }}" hint-placeholder-val="true"><div class="roslot-day-category-list"><sc-for list="{{ roslotDailyCategories }}" as="category" hint-placeholder-count="3">
4000                  <span class="roslot-day-category"><strong>{{ category.quantity }}</strong><em>{{ category.label }} · {{ category.lotsLabel }}</em></span>
4001                </sc-for></div></sc-if>
4002                <sc-if value="{{ roslotDailyCategoriesEmpty }}" hint-placeholder-val="false"><div class="roslot-day-summary-empty">No hay lotes programados para hoy.</div></sc-if>
4003              </div>
4004            </article>
4005            <aside class="roslot-live-tower" aria-label="Actividad en vivo">
4006              <div class="roslot-live-head"><div class="roslot-live-title"><small>EVENTOS PERSONALES</small><h2>Historial de hoy</h2></div><span class="roslot-live-state"><i></i>En línea</span></div>
4007              <sc-if value="{{ roslotActivityLoading }}" hint-placeholder-val="false"><div class="roslot-live-empty">Sincronizando tu actividad reciente…</div></sc-if>
4008              <sc-if value="{{ roslotHasActivity }}" hint-placeholder-val="true"><div class="roslot-live-list"><sc-for list="{{ roslotActivity }}" as="event" hint-placeholder-count="4"><button class="roslot-live-row {{ event.rowClass }}" onClick="{{ event.open }}"><span class="roslot-live-icon" style="background:{{ event.iconBg }};"><img data-dc-attr-src="{{ event.icon }}" alt=""></span><span class="roslot-live-copy"><strong>{{ event.title }}</strong><p>{{ event.body }}</p><span class="roslot-live-meta"><span>{{ event.time }}</span><span style="color:{{ event.statusColor }};">{{ event.status }}</span></span></span></button></sc-for></div></sc-if>
4009              <sc-if value="{{ roslotActivityEmpty }}" hint-placeholder-val="false"><div class="roslot-live-empty">Sin novedades de hoy. Esta torre mostrará superaciones, ofertas recibidas y cierres del día.</div></sc-if>
4010              <div class="roslot-live-footer"><span>{{ roslotRealtimeLabel }}</span><button onClick="{{ goNotificationCenter }}">Ver historial</button></div>
4011            </aside>
4012          </section>
4013        </sc-if>
4014        <!-- search + filter bar -->
4015        <div class="roslot-search-toolbar" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; position:sticky; top:{{ poolBarTop }}; z-index:30; background:rgba(244,247,251,0.92); backdrop-filter:blur(12px); padding:10px 0; border-bottom:1px solid #e3eaf2;">
4016          <div class="roslot-search-box" style="flex:1; min-width:220px; display:flex; align-items:center; gap:10px; background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:11px 14px; box-shadow:0 4px 14px rgba(0,30,60,0.05);">
4017            <span style="color:#8593a6; font-size:15px;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span>
4018            <input value="" data-dc-attr-value="{{ poolQuery }}" onInput="{{ onPoolQuery }}" placeholder="Buscar por categoría, raza, provincia o certificado…" style="border:none; outline:none; font-size:14px; font-family:inherit; width:100%; background:none; color:#112033;">
4019          </div>
4020          <button class="roslot-filter-trigger" onClick="{{ toggleFilters }}" style="display:inline-flex; align-items:center; gap:8px; background:{{ filterBtnBg }}; color:{{ filterBtnColor }}; border:1px solid {{ filterBtnBorder }}; border-radius:12px; padding:11px 18px; font-size:14px; font-weight:600; font-family:inherit; cursor:pointer; transition:transform 160ms;" style-hover="transform:translateY(-2px);">
4021            <img src="assets/icons/icon-filter.svg" alt="" style="width:15px; height:15px; filter:{{ filterIconFilter }};">Filtros
4022            <sc-if value="{{ activeFilterCount }}" hint-placeholder-val="{{ false }}"><span style="background:#e63946; color:#fff; font-size:11px; font-weight:800; min-width:19px; height:19px; border-radius:100px; display:grid; place-items:center;">{{ activeFilterCount }}</span></sc-if>
4023          </button>
4024          <select onChange="{{ onSort }}" value="" data-dc-attr-value="{{ poolSort }}" style="background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:11px 12px; font-size:13.5px; font-family:inherit; color:#112033; outline:none;">
4025            <option value="recientes">Más recientes</option>
4026            <option value="precio-asc">Precio: menor a mayor</option>
4027            <option value="precio-desc">Precio: mayor a menor</option>
4028            <option value="peso">Peso promedio</option>
4029            <option value="cabezas">Cantidad de cabezas</option>
4030          </select>
4031          <div class="roslot-view-toggle" role="group" aria-label="Vista de lotes">
4032            <span onClick="{{ setPoolGridView }}" title="Grilla" role="button" aria-label="Ver lotes en grilla" aria-pressed="{{ poolGridPressed }}" style="background:{{ poolGridViewBg }}; color:{{ poolGridViewColor }};"><i class="dc-lucide dc-lucide-grid" aria-hidden="true"></i></span>
4033            <span onClick="{{ setPoolListView }}" title="Lista" role="button" aria-label="Ver lotes en lista" aria-pressed="{{ poolListPressed }}" style="background:{{ poolListViewBg }}; color:{{ poolListViewColor }};"><i class="dc-lucide dc-lucide-list" aria-hidden="true"></i></span>
4034          </div>
4035        </div>
4036        <sc-if value="{{ showPoolOfferTracker }}" hint-placeholder-val="{{ true }}">
4037          <div style="display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap; margin-top:14px; padding:13px 16px; background:#fff; border:1px solid #e3eaf2; border-radius:14px; box-shadow:0 6px 20px rgba(0,30,60,0.05);">
4038            <div style="display:flex; align-items:center; gap:10px; min-width:220px;">
4039              <span style="width:34px; height:34px; border-radius:10px; background:#eef4fb; color:#0a5ca8; display:grid; place-items:center; flex-shrink:0;">
4040                <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg>
4041              </span>
4042              <div><div style="font-size:11px; letter-spacing:0.1em; color:#8593a6; font-weight:800;">SEGUIMIENTO DE OFERTAS</div><div style="font-size:13.5px; color:#112033; font-weight:700; margin-top:2px;">{{ poolOfferTrackerText }}</div></div>
4043            </div>
4044            <div style="display:flex; align-items:center; gap:7px; flex-wrap:wrap;">
4045              <span style="display:inline-flex; align-items:center; gap:6px; background:rgba(31,122,99,0.14); color:#1f7a63; border-radius:9px; padding:8px 11px; font-size:11.5px; font-weight:800;">
4046                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg>Vas ganando
4047              </span>
4048              <span style="display:inline-flex; align-items:center; gap:6px; background:rgba(230,57,70,0.14); color:#c22; border-radius:9px; padding:8px 11px; font-size:11.5px; font-weight:800;">
4049                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"></polyline><polyline points="15 17 21 17 21 11"></polyline></svg>Te superaron
4050              </span>
4051            </div>
4052          </div>
4053        </sc-if>
4054        <!-- filters + results -->
4055        <div class="roslot-results-layout">
4056          <aside class="roslot-filter-sidebar {{ filterSidebarClass }}" aria-label="Filtros de lotes">
4057            <div class="roslot-filter-sidebar-head"><strong>Filtros</strong><sc-if value="{{ activeFilterCount }}" hint-placeholder-val="false"><span>{{ activeFilterCount }} activos</span></sc-if></div>
4058            <sc-for list="{{ filterGroups }}" as="g" hint-placeholder-count="4">
4059              <div>
4060                <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:8px;">{{ g.label }}</div>
4061                <div style="display:flex; gap:8px; flex-wrap:wrap;">
4062                  <sc-for list="{{ g.options }}" as="o" hint-placeholder-count="4">
4063                    <span onClick="{{ o.toggle }}" style="padding:7px 14px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }}; transition:all 160ms;" style-hover="border-color:#0a5ca8;">{{ o.label }}</span>
4064                  </sc-for>
4065                </div>
4066              </div>
4067            </sc-for>
4068            <div style="display:flex; flex-direction:column; align-items:flex-start; gap:8px; border-top:1px solid #eef3f9; padding-top:12px;">
4069              <span onClick="{{ toggleChina }}" style="display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:100px; font-size:11.5px; font-weight:650; line-height:1; cursor:pointer; border:1px solid {{ chinaBorder }}; background:{{ chinaBg }}; color:{{ chinaColor }};"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="14" height="10" style="display:block; width:14px; height:10px; object-fit:cover; border-radius:2px;">Apto China</span>
4070              <span onClick="{{ clearFilters }}" style="display:block; padding-left:2px; font-size:11.5px; line-height:1.2; color:#e63946; font-weight:650; cursor:pointer;">Limpiar filtros</span>
4071            </div>
4072          </aside>
4073          <main class="roslot-results-main">
4074        <!-- results -->
4075        <div style="display:flex; justify-content:space-between; align-items:baseline; padding:18px 2px 14px;">
4076          <span style="font-size:13.5px; color:#65758a;"><strong style="color:#112033; font-variant-numeric:tabular-nums;">{{ resultCount }}</strong> {{ resultLabel }}</span>
4077          <sc-if value="{{ lotsSourceNote }}" hint-placeholder-val="{{ false }}"><span style="font-size:12px; color:#8593a6;">{{ lotsSourceNote }}</span></sc-if>
4078        </div>
4079        <sc-if value="{{ lotsLoading }}" hint-placeholder-val="{{ false }}">
4080          <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(272px, 1fr)); gap:18px;">
4081            <sc-for list="{{ skeletons }}" as="k" hint-placeholder-count="6">
4082              <div style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.06);">
4083                <div style="height:170px; background:linear-gradient(90deg, #eef3f9 25%, #f8fafd 50%, #eef3f9 75%); background-size:800px 100%; animation:shimmer 1.4s linear infinite;"></div>
4084                <div style="padding:16px; display:flex; flex-direction:column; gap:10px;">
4085                  <div style="height:14px; width:70%; border-radius:6px; background:#eef3f9;"></div>
4086                  <div style="height:11px; width:45%; border-radius:6px; background:#eef3f9;"></div>
4087                </div>
4088              </div>
4089            </sc-for>
4090          </div>
4091        </sc-if>
4092        <sc-if value="{{ showLots }}" hint-placeholder-val="{{ true }}">
4093          <div class="roslot-lot-results-grid {{ poolViewClass }}" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(272px, 1fr)); gap:18px;">
4094            <sc-for list="{{ lotCards }}" as="l" hint-placeholder-count="6">
4095              <div class="roslot-lot-card" data-reveal onMouseEnter="{{ l.hoverIn }}" onMouseLeave="{{ l.hoverOut }}" style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); display:flex; flex-direction:column; transition:transform 240ms cubic-bezier(0.22,0.72,0.18,1), box-shadow 240ms;" style-hover="transform:translateY(-6px); box-shadow:0 26px 60px rgba(0,30,60,0.18);">
4096                <div class="roslot-lot-card-media" onClick="{{ l.open }}" style="position:relative; height:180px; cursor:pointer; overflow:hidden;">
4097                  <div data-lot-img style="position:absolute; inset:0; background-image:url('{{ l.image }}'); background-size:cover; background-position:center; transition:transform 600ms cubic-bezier(0.22,0.72,0.18,1);"></div>
4098                  <sc-if value="{{ l.videoPreview }}" hint-placeholder-val="{{ false }}">
4099                    <video data-lot-preview data-src="{{ l.videoPreview }}" muted loop playsinline preload="none" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; opacity:0; transition:opacity 300ms;"></video>
4100                  </sc-if>
4101                  <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 40%, rgba(0,29,61,0.78));"></div>
4102                  <span style="position:absolute; top:10px; left:10px; background:rgba(0,29,61,0.82); color:#fff; font-size:11px; font-weight:700; padding:5px 10px; border-radius:8px; letter-spacing:0.04em;">CERT. {{ l.cert }}</span>
4103                  <sc-if value="{{ l.isLive }}" hint-placeholder-val="{{ false }}">
4104                    <span style="position:absolute; top:10px; right:10px; display:inline-flex; align-items:center; gap:6px; background:rgba(230,57,70,0.94); color:#fff; font-size:10.5px; font-weight:800; padding:5px 10px; border-radius:100px; letter-spacing:0.06em;"><span style="width:6px; height:6px; border-radius:50%; background:#fff; animation:pulseDot 1.2s infinite;"></span>EN VIVO</span>
4105                  </sc-if>
4106                  <sc-if value="{{ l.isSold }}" hint-placeholder-val="{{ false }}">
4107                    <span style="position:absolute; top:10px; right:10px; display:inline-flex; align-items:center; gap:6px; background:rgba(31,122,99,0.96); color:#fff; font-size:10.5px; font-weight:800; padding:5px 10px; border-radius:100px; letter-spacing:0.06em;"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:13px;height:13px;filter:brightness(0) invert(1);">VENDIDO</span>
4108                  </sc-if>
4109                  <div style="position:absolute; bottom:10px; left:12px; right:12px; display:flex; justify-content:space-between; align-items:flex-end; gap:8px; color:#fff;">
4110                    <div><div style="font-size:16px; font-weight:800; line-height:1.15;">{{ l.categoria }}</div><div style="font-size:11.5px; color:#c8d8ea;">{{ l.origen }}</div></div>
4111                    <div class="roslot-price-badge {{ l.priceBadgeClass }}" style="text-align:right; background:rgba(255,255,255,0.14); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.24); border-radius:10px; padding:6px 10px;">
4112                      <div style="display:flex;align-items:center;justify-content:flex-end;gap:5px;font-size:8px; color:#dbe6f2; letter-spacing:0.08em; font-weight:700;"><sc-if value="{{ l.hasOffers }}" hint-placeholder-val="false"><span class="roslot-price-live-dot"></span></sc-if>{{ l.priceLabel }}</div>
4113                      <div style="font-size:15px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap;">{{ l.currentPrice }}</div>
4114                    </div>
4115                  </div>
4116                </div>
4117                <div class="roslot-lot-card-body" style="padding:14px 16px 16px; display:flex; flex-direction:column; gap:9px; flex:1;">
4118                  <div class="roslot-lot-card-stats" style="display:flex; gap:8px;">
4119                    <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.06em;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.cabezas }}</div></div>
4120                    <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.06em;">PESO PROM.</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.peso }}</div></div>
4121                  </div>
4122                  <div class="roslot-lot-card-details" style="display:flex; flex-direction:column; gap:5px; font-size:12.5px;">
4123                    <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Raza</span><span style="color:#112033; font-weight:600; text-align:right;">{{ l.razas }}</span></div>
4124                     <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Destino</span><span style="color:{{ l.destinoColor }}; font-weight:700;">{{ l.destino }}</span></div>
4125                     <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Plazo</span><span style="color:#112033; font-weight:600;">{{ l.plazo }}</span></div>
4126                     <sc-if value="{{ l.showPlazoDetalle }}" hint-placeholder-val="false"><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Detalle del plazo</span><span style="color:#112033; font-weight:600; text-align:right;">{{ l.plazoDetalle }}</span></div></sc-if>
4127                     <div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Condición</span><span style="color:#112033; font-weight:600; text-align:right;">{{ l.condicion }}</span></div>
4128                   </div>
4129                  <sc-if value="{{ l.aptoChina }}" hint-placeholder-val="{{ false }}">
4130                    <span class="roslot-lot-card-china" style="width:fit-content; display:inline-flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; color:#9d2235; background:#fdeef0; border:1px solid #f6ccd2; padding:4px 10px; border-radius:100px;"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="16" height="12" style="display:block; width:16px; height:12px; object-fit:cover; border-radius:2px; box-shadow:0 0 0 1px rgba(157,34,53,0.16);">Apto China</span>
4131                  </sc-if>
4132                  <div class="roslot-lot-card-actions" style="display:flex; gap:8px; margin-top:auto; padding-top:6px;">
4133                    <sc-if value="{{ l.standing }}" hint-placeholder-val="{{ false }}">
4134                      <span style="flex:1; display:inline-flex; align-items:center; justify-content:center; gap:7px; background:{{ l.standingBg }}; color:{{ l.standingColor }}; border-radius:10px; padding:11px; font-size:13px; font-weight:800; animation:badgePop 340ms cubic-bezier(0.34,1.56,0.64,1) both;">
4135                        <sc-if value="{{ l.standingWinning }}" hint-placeholder-val="{{ true }}"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg></sc-if>
4136                        <sc-if value="{{ l.standingOutbid }}" hint-placeholder-val="{{ false }}"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"></polyline><polyline points="15 17 21 17 21 11"></polyline></svg></sc-if>
4137                        {{ l.standingLabel }}
4138                      </span>
4139                    </sc-if>
4140                        <button onClick="{{ l.open }}" style="flex:1; display:inline-flex;align-items:center;justify-content:center;gap:6px;background:#eef3f9; color:#002e5f; border:1px solid #dde7f2; border-radius:10px; padding:11px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#dde7f2;">Ver lote <i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></button>
4141                  </div>
4142                </div>
4143              </div>
4144            </sc-for>
4145          </div>
4146        </sc-if>
4147        <sc-if value="{{ showEmpty }}" hint-placeholder-val="{{ false }}">
4148          <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:48px 24px; text-align:center; color:#65758a;">
4149            <div style="font-size:17px; font-weight:700; color:#112033;">{{ poolEmptyTitle }}</div>
4150            <div style="font-size:13.5px; margin-top:4px;">{{ poolEmptyText }}</div>
4151            <button onClick="{{ clearFilters }}" style="margin-top:16px; background:#002e5f; color:#fff; border:none; border-radius:10px; padding:11px 22px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Limpiar filtros</button>
4152          </div>
4153        </sc-if>
4154          </main>
4155        </div>
4156        <div style="height:34px;"></div>
4157      </div>
4158    </sc-if>
4159
4160    <!-- ============ ROSLOT DAILY COMMERCIAL SUMMARY ============ -->
4161    <sc-if value="{{ isRoslotSummary }}" hint-placeholder-val="{{ false }}">
4162      <main class="roslot-report-page">
4163        <header class="roslot-report-header">
4164          <div>
4165            <button class="roslot-report-back" onClick="{{ goRoslotPool }}"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Volver al mercado</button>
4166            <div class="roslot-report-eyebrow">ROSLOT · CIERRE COMERCIAL</div>
4167            <h1>Resumen de compras y ventas</h1>
4168            <p>Una lectura clara de las operaciones adjudicadas, con los datos de contacto de cada contraparte.</p>
4169          </div>
4170          <div class="roslot-report-date-control">
4171            <label for="roslot-report-date">Fecha de operaciones</label>
4172            <div>
4173              <input id="roslot-report-date" type="date" value="{{ roslotReportDate }}" onChange="{{ onRoslotReportDate }}">
4174              <button onClick="{{ refreshRoslotReport }}" title="Actualizar resumen"><i class="dc-lucide dc-lucide-refresh-cw" aria-hidden="true"></i><span>Actualizar</span></button>
4175            </div>
4176          </div>
4177        </header>
4178
4179        <sc-if value="{{ roslotReportLoading }}" hint-placeholder-val="{{ true }}">
4180          <section class="roslot-report-loading" aria-label="Preparando resumen comercial">
4181            <div class="roslot-report-loading-hero"></div>
4182            <div class="roslot-report-loading-grid"><span></span><span></span><span></span><span></span></div>
4183            <div class="roslot-report-loading-ledger"></div>
4184          </section>
4185        </sc-if>
4186
4187        <sc-if value="{{ roslotReportHasError }}" hint-placeholder-val="{{ false }}">
4188          <section class="roslot-report-error">
4189            <span><i class="dc-lucide dc-lucide-circle-alert" aria-hidden="true"></i></span>
4190            <div><strong>No pudimos preparar el resumen</strong><p>{{ roslotReportError }}</p></div>
4191            <button onClick="{{ refreshRoslotReport }}">Reintentar</button>
4192          </section>
4193        </sc-if>
4194
4195        <sc-if value="{{ roslotReportReady }}" hint-placeholder-val="{{ false }}">
4196          <section class="roslot-report-hero">
4197            <div class="roslot-report-hero-copy">
4198              <span>{{ roslotReportScopeLabel }}</span>
4199              <h2>{{ roslotReportHeadline }}</h2>
4200              <p>{{ roslotReportHeadlineDetail }}</p>
4201            </div>
4202            <div class="roslot-report-hero-date"><small>Jornada</small><strong>{{ roslotReportDateLabel }}</strong></div>
4203            <div class="roslot-report-hero-note"><i class="dc-lucide dc-lucide-shield-check" aria-hidden="true"></i><p><strong>Información privada</strong><span>Solo se muestran operaciones propias, salvo perfiles administradores.</span></p></div>
4204          </section>
4205
4206          <section class="roslot-report-kpis">
4207            <sc-for list="{{ roslotReportKpis }}" as="kpi" hint-placeholder-count="4">
4208              <article class="roslot-report-kpi"><span><i class="dc-lucide {{ kpi.icon }}" aria-hidden="true"></i></span><div><small>{{ kpi.label }}</small><strong>{{ kpi.value }}</strong><p>{{ kpi.detail }}</p></div></article>
4209            </sc-for>
4210          </section>
4211
4212          <section class="roslot-report-ledger">
4213            <header class="roslot-report-ledger-head">
4214              <div><span>DETALLE OPERATIVO</span><h2>Operaciones adjudicadas</h2><p>Precio, volumen y contacto comercial en una sola lectura.</p></div>
4215              <div class="roslot-report-search"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i><input type="search" placeholder="Buscar lote o contraparte…" value="{{ roslotReportSearch }}" onInput="{{ onRoslotReportSearch }}"></div>
4216            </header>
4217            <sc-if value="{{ roslotReportShowRoleFilters }}" hint-placeholder-val="{{ true }}">
4218              <nav class="roslot-report-filters" aria-label="Filtrar operaciones">
4219                <sc-for list="{{ roslotReportFilters }}" as="filter" hint-placeholder-count="3"><button class="roslot-report-filter{{ filter.cls }}" onClick="{{ filter.go }}">{{ filter.label }}</button></sc-for>
4220              </nav>
4221            </sc-if>
4222
4223            <div class="roslot-report-transactions">
4224              <sc-for list="{{ roslotReportRows }}" as="row" hint-placeholder-count="2">
4225                <article class="roslot-report-row">
4226                  <div class="roslot-report-row-thumb">
4227                    <sc-if value="{{ row.hasThumbnail }}" hint-placeholder-val="{{ true }}"><img data-dc-attr-src="{{ row.thumbnail }}" alt=""></sc-if>
4228                    <sc-if value="{{ row.hasNoThumbnail }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-beef" aria-hidden="true"></i></sc-if>
4229                    <span>VENDIDO</span>
4230                  </div>
4231                  <div class="roslot-report-row-lot">
4232                    <small>CERT. {{ row.cert }}</small>
4233                    <h3>{{ row.category }}</h3>
4234                    <p>{{ row.establishment }} · {{ row.location }}</p>
4235                    <div><span>{{ row.heads }} cab.</span><span>{{ row.weight }}</span></div>
4236                  </div>
4237                  <div class="roslot-report-row-money">
4238                    <small>IMPORTE ESTIMADO</small><strong>{{ row.total }}</strong>
4239                    <p>{{ row.price }} <em>{{ row.priceUnit }}</em></p>
4240                    <span>{{ row.condition }} · {{ row.closedTime }}</span>
4241                  </div>
4242                  <section class="roslot-report-row-contact is-buyer">
4243                    <header><span>{{ row.buyer.initials }}</span><div><small>{{ row.buyer.type }}</small><strong>{{ row.buyer.name }}</strong></div></header>
4244                    <div class="roslot-report-row-contact-data"><span title="{{ row.buyer.phone }}">{{ row.buyer.phone }}</span><span title="{{ row.buyer.email }}">{{ row.buyer.email }}</span></div>
4245                    <div class="roslot-report-row-contact-actions">
4246                      <sc-if value="{{ row.buyer.hasPhone }}" hint-placeholder-val="{{ true }}"><a href="{{ row.buyer.phoneHref }}" aria-label="Llamar a {{ row.buyer.name }}"><i class="dc-lucide dc-lucide-phone" aria-hidden="true"></i>Llamar</a></sc-if>
4247                      <sc-if value="{{ row.buyer.hasEmail }}" hint-placeholder-val="{{ true }}"><a href="{{ row.buyer.emailHref }}" aria-label="Enviar email a {{ row.buyer.name }}"><i class="dc-lucide dc-lucide-mail" aria-hidden="true"></i>Email</a></sc-if>
4248                    </div>
4249                  </section>
4250                  <section class="roslot-report-row-contact is-seller">
4251                    <header><span>{{ row.seller.initials }}</span><div><small>{{ row.seller.type }}</small><strong>{{ row.seller.name }}</strong></div></header>
4252                    <div class="roslot-report-row-contact-data"><span title="{{ row.seller.phone }}">{{ row.seller.phone }}</span><span title="{{ row.seller.email }}">{{ row.seller.email }}</span></div>
4253                    <div class="roslot-report-row-contact-actions">
4254                      <sc-if value="{{ row.seller.hasPhone }}" hint-placeholder-val="{{ true }}"><a href="{{ row.seller.phoneHref }}" aria-label="Llamar a {{ row.seller.name }}"><i class="dc-lucide dc-lucide-phone" aria-hidden="true"></i>Llamar</a></sc-if>
4255                      <sc-if value="{{ row.seller.hasEmail }}" hint-placeholder-val="{{ true }}"><a href="{{ row.seller.emailHref }}" aria-label="Enviar email a {{ row.seller.name }}"><i class="dc-lucide dc-lucide-mail" aria-hidden="true"></i>Email</a></sc-if>
4256                    </div>
4257                  </section>
4258                  <button class="roslot-report-row-open" onClick="{{ row.open }}">Ver lote<i class="dc-lucide dc-lucide-arrow-up-right" aria-hidden="true"></i></button>
4259                </article>
4260              </sc-for>
4261            </div>
4262
4263            <sc-if value="{{ roslotReportEmpty }}" hint-placeholder-val="{{ false }}">
4264              <div class="roslot-report-empty"><span><i class="dc-lucide dc-lucide-calendar-check" aria-hidden="true"></i></span><h3>No hay operaciones para mostrar</h3><p>No encontramos compras o ventas adjudicadas para esta fecha y este filtro.</p></div>
4265            </sc-if>
4266          </section>
4267        </sc-if>
4268      </main>
4269    </sc-if>
4270
4271    <!-- ============ LOT DETAIL ============ -->
4272    <sc-if value="{{ isLot }}" hint-placeholder-val="{{ false }}">
4273      <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;">
4274        <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:14px; flex-wrap:wrap; padding:24px 0 18px;">
4275          <div>
4276            <span onClick="{{ goPool }}" style="display:inline-flex;align-items:center;gap:6px;font-size:13px; color:#0a5ca8; font-weight:600; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al pool</span>
4277            <h1 style="font-size:clamp(23px, 3.2vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:8px 0 2px; color:#112033;">{{ lotTitle }}</h1>
4278            <span style="font-size:13.5px; color:#65758a;">Cert. {{ lotCert }} &middot; {{ lotOrigen }}</span>
4279          </div>
4280          <span style="display:inline-flex; align-items:center; gap:8px; background:{{ lotStatusBg }}; color:#fff; font-size:12px; font-weight:800; padding:7px 16px; border-radius:100px; letter-spacing:0.06em;"><img data-dc-attr-src="{{ lotStatusIcon }}" alt="" width="15" height="15" style="filter:brightness(0) invert(1);">{{ lotStatus }}</span>
4281        </div>
4282        <div style="background:#fff; border:1px solid #e1e9f1; border-radius:16px; padding:14px 16px; margin-bottom:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
4283          <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;">
4284            <span style="display:inline-flex; align-items:center; gap:7px; padding:6px 10px; border-radius:100px; background:{{ marketStatusBg }}; color:{{ marketStatusColor }}; border:1px solid {{ marketStatusBorder }}; font-size:11.5px; font-weight:800; letter-spacing:.02em;">
4285              <img data-dc-attr-src="{{ marketStatusIcon }}" alt="" aria-hidden="true" width="14" height="14" style="width:14px; height:14px;">
4286              {{ marketStatusLabel }}
4287            </span>
4288            <strong data-roslot-market-countdown style="font-size:11.5px; color:{{ marketTimerColor }}; font-variant-numeric:tabular-nums;">{{ marketCountdownLabel }}</strong>
4289          </div>
4290          <div data-roslot-market-progress role="progressbar" aria-label="Tiempo hasta el próximo cambio del mercado" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ marketProgressValue }}" style="height:6px; margin-top:9px; overflow:hidden; border-radius:100px; background:#dfe7f0; box-shadow:inset 0 1px 2px rgba(17,32,51,.08);">
4291            <span data-roslot-market-progress-fill style="display:block; width:{{ marketProgress }}; height:100%; border-radius:inherit; background:{{ marketProgressBg }}; transition:width 1s linear;"></span>
4292          </div>
4293          <small data-roslot-market-schedule style="display:block; margin-top:6px; color:#687a8f; font-size:10.5px; font-weight:650;">{{ marketScheduleLabel }}</small>
4294        </div>
4295        <div style="display:grid; grid-template-columns:{{ lotLayoutCols }}; gap:20px; align-items:start;">
4296          <!-- media -->
4297          <div style="display:flex; flex-direction:column; gap:12px;">
4298            <div class="dc-cabana-video-frame" style="border-radius:20px; box-shadow:0 22px 60px rgba(0,30,60,0.2);">
4299              <sc-if value="{{ lotVideoUrl }}" hint-placeholder-val="{{ false }}">
4300                <video class="roslot-lot-player" data-lot-player data-src="{{ lotVideoUrl }}" onLoadedMetadata="{{ lotVideoMeta }}" onTimeUpdate="{{ lotVideoTimeUpdate }}" onVolumeChange="{{ lotVideoVolumeChange }}" onPause="{{ lotVideoPause }}" onPlay="{{ lotVideoPlay }}" onPlaying="{{ lotVideoPlay }}" onEnded="{{ lotVideoEnded }}" controls autoplay muted playsinline preload="auto" style="display:block; width:100%; height:100%; object-fit:cover; background:#001d3d;"></video>
4301              </sc-if>
4302              <sc-if value="{{ lotNoVideo }}" hint-placeholder-val="{{ true }}">
4303                <div style="width:100%; aspect-ratio:16/9; background-image:url('{{ lotImage }}'); background-size:cover; background-position:center;"></div>
4304              </sc-if>
4305            </div>
4306            <sc-if value="{{ lotVideoUrl }}" hint-placeholder-val="{{ false }}"><div class="dc-video-controls" aria-label="Controles del video"><button class="dc-video-control-button" onClick="{{ lotTogglePlay }}" title="{{ lotPlayLabel }}" aria-label="{{ lotPlayLabel }}"><img data-dc-attr-src="{{ lotPlayIcon }}" alt=""></button><input class="dc-video-slider" type="range" min="0" max="100" step="0.1" value="{{ lotVideoProgress }}" onInput="{{ lotSeekVideo }}" onChange="{{ lotSeekVideo }}" aria-label="Avance del video"><span class="dc-video-time">{{ lotVideoTime }}</span><button class="dc-video-control-button" onClick="{{ lotToggleMute }}" title="{{ lotMuteLabel }}" aria-label="{{ lotMuteLabel }}"><img data-dc-attr-src="{{ lotVolumeIcon }}" alt=""></button><input class="dc-video-slider dc-video-volume" type="range" min="0" max="1" step="0.05" value="{{ lotVideoVolume }}" onInput="{{ lotSetVolume }}" onChange="{{ lotSetVolume }}" aria-label="Volumen"><button class="dc-video-control-button" onClick="{{ lotFullscreen }}" title="Pantalla completa" aria-label="Pantalla completa"><img src="assets/icons/lucide/maximize-2.svg" alt=""></button></div></sc-if>
4307            <sc-if value="{{ hasVideoThumbs }}
4307" hint-placeholder-val="{{ false }}">
4308              <div style="display:flex; gap:10px; overflow-x:auto; padding-bottom:4px;">
4309                <sc-for list="{{ videoThumbs }}" as="vt" hint-placeholder-count="3">
4310                  <div onClick="{{ vt.select }}" style="position:relative; width:110px; aspect-ratio:16/10; border-radius:10px; overflow:hidden; cursor:pointer; flex-shrink:0; border:2px solid {{ vt.border }}; transition:transform 180ms;" style-hover="transform:translateY(-2px);">
4311                    <sc-if value="{{ vt.hasThumb }}" hint-placeholder-val="{{ true }}">
4312                      <img data-dc-attr-src="{{ vt.thumb }}" alt="Video {{ vt.n }}" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover;">
4313                    </sc-if>
4314                    <sc-if value="{{ vt.loading }}" hint-placeholder-val="{{ false }}">
4315                      <div style="position:absolute; inset:0; display:grid; place-items:center; background:#e8eef5;"><span style="width:19px; height:19px; border:2px solid #9fb0c3; border-top-color:#0a5ca8; border-radius:50%; animation:spin 800ms linear infinite;"></span></div>
4316                    </sc-if>
4317                    <span style="position:absolute; bottom:4px; right:6px; color:#fff; font-size:11px; font-weight:800; text-shadow:0 1px 4px rgba(0,0,0,0.6);">{{ vt.n }}</span>
4318                  </div>
4319                </sc-for>
4320              </div>
4321            </sc-if>
4322            <!-- data groups -->
4323            <sc-for list="{{ lotGroups }}" as="g" hint-placeholder-count="3">
4324              <div data-reveal style="background:#fff; border-radius:18px; padding:18px 20px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
4325                <div style="font-size:12px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:12px;">{{ g.label }}</div>
4326                <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:10px;">
4327                  <sc-for list="{{ g.cards }}" as="d" hint-placeholder-count="4">
4328                    <div style="background:#f6f9fc; border-radius:12px; padding:11px 13px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.06em;">{{ d.label }}</div><div style="font-size:15px; font-weight:700; color:#112033; margin-top:2px;">{{ d.value }}</div></div>
4329                  </sc-for>
4330                </div>
4331              </div>
4332            </sc-for>
4333            <div data-reveal style="background:#fff; border-radius:18px; padding:18px 20px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
4334              <div style="font-size:12px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:10px;">OBSERVACIONES</div>
4335              <p style="margin:0; font-size:14px; color:#3d4c5f; line-height:1.55;">{{ lotObs }}</p>
4336            </div>
4337          </div>
4338          <!-- bid panel -->
4339          <div style="display:flex; flex-direction:column; gap:12px; position:{{ bidPanelPos }}; top:76px;">
4340            <div style="background:#fff; border-radius:16px; padding:14px 16px; box-shadow:0 8px 26px rgba(0,30,60,0.07); display:flex; align-items:center; gap:12px;">
4341              <span style="width:42px; height:42px; flex:0 0 42px; border-radius:12px; background:{{ lotPublisherAvatarBg }}; color:#fff; display:grid; place-items:center; box-sizing:border-box; padding:5px; font-size:13px; font-weight:900; overflow:hidden;">
4342                <sc-if value="{{ lotPublisherHasLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lotPublisherLogo }}" alt="{{ lotPublisherName }}" style="display:block; width:100%; height:100%; object-fit:contain;"></sc-if>
4343                <sc-if value="{{ lotPublisherNoLogo }}" hint-placeholder-val="true">{{ lotPublisherInitials }}</sc-if>
4344              </span>
4345              <div style="min-width:0;"><div style="font-size:11px; color:#8593a6;">
4345Publicado por</div><strong style="font-size:14px; color:#112033;">{{ lotPublisherName }}</strong></div>
4346              <div style="margin-left:auto; text-align:right;"><div style="font-size:11px; color:#8593a6;">Vistas</div><strong style="font-size:15px; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ lotViews }}</strong></div>
4347            </div>
4348            <div class="roslot-bid-card" style="background:linear-gradient(160deg, #001d3d, #0a3e78); border-radius:20px; padding:22px; color:#fff; box-shadow:0 22px 60px rgba(0,30,60,0.3);">
4349              <div style="font-size:11.5px; letter-spacing:0.12em; color:#9fc0e2; font-weight:700;">{{ bidPriceTitle }}</div>
4350              <div style="font-size:36px; font-weight:900; letter-spacing:-0.02em; margin:6px 0 2px; font-variant-numeric:tabular-nums; display:flex; align-items:baseline; gap:4px;">{{ currentBidRoll }}</div>
4351              <div style="font-size:12px; color:#9fc0e2;">{{ bidPriceSubtitle }}</div>
4352              <div style="height:1px; background:rgba(255,255,255,0.14); margin:16px 0;"></div>
4353              <sc-if value="{{ bidControlsVisible }}" hint-placeholder-val="true">
4354              <div style="display:flex; gap:10px; align-items:stretch; opacity:{{ bidControlsOpacity }}; pointer-events:{{ bidControlsPointer }};">
4355                <div style="display:flex; flex-direction:column; gap:6px;">
4356                  <button onClick="{{ bidPlus }}" style="width:44px; flex:1; background:{{ plusBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); border-radius:10px; font-size:19px; font-weight:800; font-family:inherit; cursor:pointer; transition:transform 140ms;" style-hover="transform:scale(1.06);" style-active="transform:scale(0.94);"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></button>
4357                  <button onClick="{{ bidMinus }}" aria-label="Disminuir oferta" style="width:44px; flex:1; background:{{ minusBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); border-radius:10px; font-size:19px; font-weight:800; font-family:inherit; cursor:pointer; transition:transform 140ms;" style-hover="transform:scale(1.06);" style-active="transform:scale(0.94);"><i class="dc-lucide dc-lucide-minus" aria-hidden="true"></i></button>
4358                </div>
4359                <div style="flex:1; display:flex; flex-direction:column; gap:6px;">
4360                  <sc-for list="{{ bidIncrements }}" as="bi" hint-placeholder-count="2">
4361                    <button onClick="{{ bi.pick }}" style="flex:1; background:{{ bi.bg }}; color:{{ bi.color }}; border:1px solid {{ bi.border }}; border-radius:10px; font-size:15px; font-weight:800; font-family:inherit; cursor:pointer; font-variant-numeric:tabular-nums; transition:all 160ms;" style-hover="border-color:#ffffff;">{{ bi.label }}</button>
4362                  </sc-for>
4363                </div>
4364              </div>
4365              <div style="margin-top:16px; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.16); border-radius:14px; padding:13px 16px; display:flex; justify-content:space-between; align-items:center;">
4366                <div><div style="font-size:11px; color:#9fc0e2;">{{ bidOffsetLabel }}</div><div style="font-size:22px; font-weight:900; font-variant-numeric:tabular-nums; display:flex; align-items:baseline; gap:3px;">{{ bidTargetRoll }}</div></div>
4367                <span style="font-size:11px; color:#9fc0e2; text-align:right;">{{ bidPriceUnitLabel }}</span>
4368              </div>
4369              <div style="margin-top:9px;padding:11px 13px;border:1px solid rgba(117,211,194,.28);border-radius:13px;background:rgba(31,122,99,.16);">
4370                <div style="margin-bottom:8px;font-size:9.5px;letter-spacing:.1em;color:#a9e7d9;font-weight:900;">ESTIMACIÓN DE TU OFERTA</div>
4371                <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:11px;color:#dceaf5;"><span>Costo / bulto</span><strong style="display:flex;align-items:baseline;justify-content:flex-end;font-size:13px;line-height:1.15;color:#fff;font-variant-numeric:tabular-nums;white-space:nowrap;">{{ bidEstimatedUnitCost }}</strong></div>
4372                <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,.1);font-size:11px;color:#dceaf5;"><span>Costo lote completo</span><strong style="display:flex;align-items:baseline;justify-content:flex-end;font-size:13px;line-height:1.15;color:#fff;font-variant-numeric:tabular-nums;white-space:nowrap;">{{ bidEstimatedTotalCost }}</strong></div>
4373                <div style="margin-top:7px;font-size:9px;line-height:1.3;color:#a9bfd6;">{{ bidEstimatedNote }}</div>
4374              </div>
4375              </sc-if>
4376              <sc-if value="{{ showSellerHammerPanel }}" hint-placeholder-val="false"><div style="padding:14px 15px;border:1px solid rgba(255,201,99,.38);border-radius:14px;background:rgba(184,109,0,.18);"><strong style="display:flex;align-items:center;gap:7px;color:#ffe0a2;font-size:14px;"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:16px;height:16px;filter:brightness(0) invert(1);">Cierre de subasta</strong><span style="display:block;margin-top:6px;color:#dceaf5;font-size:11.5px;line-height:1.45;">{{ sellerHammerMessage }}</span></div></sc-if>
4377              <sc-if value="{{ showWinningNotice }}" hint-placeholder-val="false"><div style="padding:14px 15px;border:1px solid rgba(117,211,194,.38);border-radius:14px;background:rgba(31,122,99,.22);"><strong style="display:flex;align-items:center;gap:7px;color:#c6f5e6;font-size:15px;"><img src="assets/icons/lucide/circle-check-big.svg" alt="" aria-hidden="true" style="width:17px;height:17px;filter:brightness(0) invert(1);">¡Vas ganando!</strong><span style="display:block;margin-top:6px;color:#dceaf5;font-size:11.5px;line-height:1.45;">Tu oferta sigue siendo la líder. No necesitás ni podés volver a ofertar mientras mantengas esta posición.</span></div></sc-if>
4378              <button onClick="{{ bidPrimaryAction }}" disabled="{{ bidDisabled }}" style="width:100%; margin-top:14px; display:flex; align-items:center; justify-content:center; gap:8px; background:{{ bidBtnBg }}; color:#fff; border:none; border-radius:14px; padding:16px; font-size:16px; font-weight:800; font-family:inherit; cursor:{{ bidBtnCursor }}; opacity:{{ bidBtnOpacity }}; letter-spacing:0.01em; transition:transform 160ms; animation:{{ bidBtnAnim }};" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.97);"><sc-if value="{{ bidShowsHammer }}" hint-placeholder-val="false"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:17px;height:17px;filter:brightness(0) invert(1);"></sc-if>{{ bidBtnLabel }}</button>
4379              <sc-if value="{{ bidMessage }}" hint-placeholder-val="{{ false }}">
4380                <div style="margin-top:10px; font-size:12.5px; text-align:center; padding:9px 12px; border-radius:10px; background:{{ bidMsgBg }}; color:{{ bidMsgColor }};">{{ bidMessage }}</div>
4381              </sc-if>
4382              <sc-if value="{{ lotBidSuccess }}" hint-placeholder-val="false">
4382<div class="roslot-op-success is-detail" role="status" aria-live="assertive" aria-label="Oferta confirmada"><dotlottie-wc class="roslot-op-success-lottie" src="assets/auth/done.lottie" autoplay speed="1.45" aria-hidden="true"></dotlottie-wc><span class="roslot-op-success-copy"><small>OFERTA CONFIRMADA</small><strong>{{ lotBidSuccessAmount }}</strong><span>Lote {{ lotCert }}</span></span></div></sc-if>
4383            </div>
4384            <sc-if value="{{ lotBidsHas }}" hint-placeholder-val="{{ false }}">
4385              <div style="background:#fff; border-radius:16px; padding:16px 18px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
4386                <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:10px;">
4387                  <div style="font-size:12px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">OFERTAS RECIBIDAS</div>
4388                  <span style="font-size:12px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ lotBidsCount }}</span>
4389                </div>
4390                <div style="display:flex; flex-direction:column;">
4391                  <sc-for list="{{ lotBidRows }}" as="b" hint-placeholder-count="3">
4392                    <div style="display:flex; align-items:center; gap:10px; padding:9px 0; border-top:1px solid #f0f4f9;">
4393                      <div style="flex:1; min-width:0;"><div style="font-size:13.5px; font-weight:700; color:#112033; font-variant-numeric:tabular-nums;">{{ b.amount }}</div><div style="font-size:11px; color:#8593a6;">{{ b.bidder }} · {{ b.date }}</div></div>
4394                      <span style="font-size:10.5px; font-weight:800; padding:4px 10px; border-radius:100px; background:{{ b.statusBg }}; color:{{ b.statusColor }};">{{ b.status }}</span>
4395                    </div>
4396                  </sc-for>
4397                </div>
4398              </div>
4399            </sc-if>
4400            <sc-if value="{{ lotBidsLoading }}" hint-placeholder-val="{{ false }}">
4401              <div style="background:#fff; border-radius:16px; padding:20px; box-shadow:0 8px 26px rgba(0,30,60,0.07); text-align:center; color:#8593a6; font-size:12.5px;">Cargando ofertas…</div>
4402            </sc-if>
4403          </div>
4404        </div>
4405        <div style="height:40px;"></div>
4406      </div>
4407    </sc-if>
4408
4409    <!-- ============ CARGAR LOTE ROSGAN ============ -->
4410    <sc-if value="{{ isRosganLotLoad }}" hint-placeholder-val="{{ false }}">
4411      <style>
4412        .rg-wizard-panel section{background:transparent!important;border:0!important;border-radius:0!important;padding:0!important;box-shadow:none!important}
4413        .rg-wizard-panel,.rg-wizard-panel *{box-sizing:border-box}
4414        .rg-wizard-layout,.rg-wizard-layout>*{min-width:0}
4415        .rg-wizard-panel>div,.rg-wizard-panel section,.rg-provenance-grid,.rg-provenance-grid>*{min-width:0;max-width:100%}
4416        .rg-wizard-panel label{font-size:12.5px!important;font-weight:600!important;color:#3d4c5f!important;letter-spacing:0!important}
4417        .rg-wizard-panel input,.rg-wizard-panel select,.rg-wizard-panel textarea{display:block;width:100%;min-width:0;max-width:100%;font-family:inherit!important;font-size:14px!important;outline:none!important}
4418        .rg-wizard-panel select{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4419        .rg-wizard-panel button{max-width:100%}
4420        .rg-field-label-row{min-width:0;flex-wrap:wrap}.rg-field-label-row button{flex:0 0 auto;white-space:nowrap}
4421        .rg-wizard-panel input:focus,.rg-wizard-panel select:focus,.rg-wizard-panel textarea:focus{border-color:#0a5ca8!important;box-shadow:0 0 0 3px rgba(10,92,168,.08)}
4422        .rg-wizard-panel section.rg-specific-panel{background:#f6f9fc!important;color:#112033!important;border:1px solid #e3eaf2!important;border-radius:14px!important;padding:18px!important;box-shadow:none!important;margin-top:18px}
4423        .rg-specific-panel input,.rg-specific-panel select{background:#fff!important;color:#112033!important;border:1px solid #c4d3e3!important;min-width:0;width:100%;box-sizing:border-box;color-scheme:light}
4424        .rg-specific-panel select{appearance:auto;cursor:pointer;font:500 13px inherit}
4425        .rg-specific-panel select option{background:#fff;color:#112033}
4426        .rg-specific-panel input:focus-visible,.rg-specific-panel select:focus-visible{outline:2px solid #0a5ca8;outline-offset:2px}
4427        .rg-specific-panel>div:first-child{color:#0a5ca8!important}.rg-specific-panel h2{color:#112033!important}
4428        .rg-characteristics-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 14px;align-items:start}
4429        .rg-characteristics-grid>*{min-width:0}
4430        .rg-characteristics-grid label,.rg-age-fields label{min-width:0;line-height:1.25;overflow-wrap:anywhere}
4431        .rg-characteristics-grid input,.rg-characteristics-grid select{display:block;width:100%;min-width:0}
4432        .rg-age-fields{grid-column:span 2;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.78fr);gap:10px;min-width:0}
4433        .rg-cons-picker{position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:6px;min-width:0;max-width:100%;overflow:visible}
4434        .rg-cons-picker-label{font-size:11px;font-weight:800;color:#60758c;letter-spacing:.05em}
4435        .rg-cons-picker-button{width:100%;min-width:0;max-width:100%;min-height:46px;display:flex;align-items:center;gap:10px;overflow:hidden;border:1px solid #ccdae8;border-radius:12px;background:#fff;color:#102f50;padding:6px 11px;text-align:left;font:600 13px inherit;cursor:pointer;transition:border-color .16s,box-shadow .16s}
4436        .rg-cons-picker-button:hover,.rg-cons-picker-button:focus{border-color:#0a5ca8;box-shadow:0 0 0 3px rgba(10,92,168,.08)}
4437        .rg-cons-picker-logo{width:31px;height:31px;flex:0 0 31px;display:grid;place-items:center;overflow:hidden;border:1px solid #e1e8f0;border-radius:9px;background:#fff;color:#0a5ca8;font-size:10px;font-weight:900}
4438        .rg-cons-picker-logo img{display:block;width:100%;height:100%;object-fit:contain;padding:3px;box-sizing:border-box}
4439        .rg-cons-picker-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4440        .rg-cons-picker-menu{position:absolute;top:calc(100% + 7px);left:0;right:0;z-index:45;display:grid;gap:4px;width:100%;max-width:100%;max-height:min(310px,55vh);overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:7px;border:1px solid #d5e1ed;border-radius:14px;background:#fff;box-shadow:0 18px 44px rgba(0,36,78,.2)}
4441        .rg-cons-picker-option{width:100%;min-width:0;max-width:100%;min-height:46px;display:flex;align-items:center;gap:10px;overflow:hidden;border:1px solid transparent;border-radius:10px;background:transparent;color:#102f50;padding:6px 8px;text-align:left;font:650 12.5px inherit;cursor:pointer}
4442        .rg-cons-picker-option:hover{background:#f0f6fb}.rg-cons-picker-option.is-selected{border-color:#b9d5ee;background:#eaf4fc;color:#07518f}
4443        .rg-provenance-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:22px 16px!important;align-items:start}
4444        .rg-provenance-field{display:grid;align-content:start;gap:7px;min-width:0}
4445        .rg-provenance-field>label,.rg-provenance-field>.rg-field-label-row,.rg-cons-picker-label{color:#536b83!important;font-size:11px!important;font-weight:850!important;letter-spacing:.07em!important;line-height:1.3;text-transform:uppercase}
4446        .rg-provenance-field>label:has(select){display:grid;align-content:start;gap:7px}
4447        .rg-provenance-field select,.rg-provenance-field input,.rg-cons-picker-button{height:48px;min-height:48px!important}
4448        .rg-provenance-help{min-height:15px;color:#718296;font-size:11px;line-height:1.35;font-weight:500;letter-spacing:0;text-transform:none}
4449        .rg-provenance-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:21px}
4450        .rg-provenance-section-head>div{display:flex;align-items:center;gap:12px;min-width:0}
4451        .rg-provenance-section-head button{flex:none}
4452        .rg-provenance-action{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;color:#0a5ca8;padding:0;font:800 11px inherit;cursor:pointer;white-space:nowrap}
4453        .rg-provenance-action:disabled{cursor:not-allowed}
4454        .rg-create-button{min-height:38px;padding:8px 12px;background:#e9f3ff;border:1px solid #bad5f0;border-radius:10px;font-size:12px}
4455        .rg-inline-create{scroll-margin-top:110px;animation:pageIn 220ms both}
4456        .rg-provenance-field:has(.rg-inline-create){grid-column:1/-1}
4457        @media(max-width:700px){.rg-create-button{min-height:46px;padding:11px 14px;background:#0a5ca8;color:#fff;font-size:14px;white-space:normal}.rg-field-label-row{flex-wrap:wrap}.rg-inline-create button{min-height:44px!important}.rg-inline-create .rg-form-grid{grid-template-columns:1fr!important}}
4458
4459        .rg-provenance-optional{grid-column:1/-1;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;padding-top:18px;border-top:1px solid #e7edf4}
4460        .rg-lot-type-choices{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px;margin-bottom:15px}
4461        .rg-lot-type-choice{min-width:0;min-height:43px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid var(--rg-type-border);border-radius:11px;background:var(--rg-type-bg);color:var(--rg-type-color);padding:8px 12px;font:800 12.5px inherit;cursor:pointer;transition:border-color .16s,background .16s,box-shadow .16s}
4462        .rg-lot-type-choice.is-selected{box-shadow:0 6px 16px rgba(10,92,168,.15)}
4463        .rg-state-toggles{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:17px}
4464        .rg-state-toggle{min-height:42px;display:inline-flex;align-items:center;justify-content:center;gap:9px;border:1px solid;border-radius:12px;padding:8px 14px;font:800 12.5px inherit;cursor:pointer;transition:transform .15s,box-shadow .15s,background .15s,border-color .15s}
4465        .rg-state-toggle:hover{transform:translateY(-1px)}
4466        .rg-state-toggle img{display:block;flex:0 0 auto;object-fit:contain}
4467        .rg-state-toggle-check{width:17px;height:17px;
4467display:grid;place-items:center;border-radius:50%;font-size:11px;font-weight:900}
4468        .rg-description-options{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px}
4469        .rg-description-choice{min-height:36px;display:inline-flex;align-items:center;gap:7px;border:1px solid;border-radius:11px;padding:7px 12px;font:750 11.5px inherit;cursor:pointer;transition:transform .14s,box-shadow .14s,background .14s,border-color .14s}
4470        .rg-payment-chip{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:40px;padding:9px 16px;border:1px solid #d4e2f0;border-radius:999px;background:#f5f8fc;color:#47617d;font-family:inherit;font-size:13px;font-weight:650;line-height:1;white-space:nowrap;cursor:pointer;transition:background .15s,border-color .15s,color .15s,box-shadow .15s}
4471        .rg-payment-chip:hover{background:#eaf2fb;border-color:#95bce2}
4472        .rg-payment-chip[aria-pressed="true"]{background:#0a5ca8;border-color:#0a5ca8;color:#fff;box-shadow:0 3px 8px #0a5ca824}
4473        .rg-payment-chip:focus-visible{outline:2px solid #0a5ca8;outline-offset:3px}
4474        .rg-payment-chip-mark{display:none;font-size:13px}.rg-payment-chip[aria-pressed="true"] .rg-payment-chip-mark{display:inline}
4475        .rg-description-choice:hover{transform:translateY(-1px);box-shadow:0 5px 12px rgba(15,58,96,.09)}
4476        .rg-description-choice-mark{width:17px;height:17px;display:grid;place-items:center;flex:0 0 17px;border-radius:5px;font-size:11px;font-weight:900}
4477        @media(max-width:1100px){.rg-wizard-layout{grid-template-columns:minmax(0,1fr)!important}.rg-preview-panel{position:static!important;width:min(420px,100%);justify-self:center}.rg-characteristics-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.rg-age-fields{grid-column:1/-1}}
4478        @media(max-width:720px){.rg-form-grid,.rg-provenance-grid,.rg-provenance-optional{grid-template-columns:1fr!important}.rg-provenance-optional{grid-column:auto}.rg-provenance-section-head{align-items:flex-start}.rg-step-copy{display:none!important}.rg-characteristics-grid{grid-template-columns:1fr}.rg-age-fields{grid-column:auto;grid-template-columns:minmax(0,1fr) minmax(0,.8fr)}.rg-wizard-panel{padding:18px 14px!important}.rg-wizard-footer{flex-wrap:wrap}.rg-wizard-footer button{flex:1 1 140px;min-width:0!important;padding-left:14px!important;padding-right:14px!important}}
4479        @media(max-width:420px){.rg-age-fields{grid-template-columns:1fr}.rg-wizard-panel input,.rg-wizard-panel select,.rg-wizard-panel textarea{font-size:16px!important}.rg-cons-picker-menu{max-height:48vh}.rg-wizard-footer button{flex-basis:100%}.rg-lot-type-choices{grid-template-columns:1fr}.rg-lot-type-choice{min-height:40px}}
4480      </style>
4481      <div style="min-height:calc(100vh - 72px); background:#f4f7fb; padding:0 clamp(14px,4vw,36px) 52px;">
4482        <div style="max-width:1240px; margin:0 auto; animation:pageIn 360ms ease both;">
4483          <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; padding:24px 0 20px;">
4484            <div>
4485              <span onClick="{{ goRemates }}" style="display:inline-flex; align-items:center; gap:6px; color:#0a5ca8; font-size:13px; font-weight:700; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver a remates</span>
4486              <h1 style="font-size:clamp(23px,3.2vw,32px); font-weight:800; letter-spacing:-.02em; margin:8px 0 2px; color:#112033;">{{ rgWizardTitle }}</h1>
4487              <a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a>
4488              <p style="margin:0; color:#65758a; font-size:13.5px;">{{ rgWizardSubtitle }}</p>
4489            </div>
4490            <div style="display:flex; align-items:center; gap:14px; flex-wrap:wrap;">
4491              <sc-if value="{{ rgCanViewPanel }}" hint-placeholder-val="{{ false }}"><button type="button" onClick="{{ openRgPanel }}" disabled="{{ rgPanelDisabled }}" title="{{ rgPanelHint }}" style="display:inline-flex; align-items:center; gap:8px; padding:11px 17px; border:1px solid #0a5ca8; border-radius:100px; background:#0a5ca8; color:#fff; font:700 13px inherit; cursor:pointer; opacity:{{ rgPanelOpacity }};"><img src="/assets/icons/lucide/clipboard-list.svg" width="17" height="17" alt="" aria-hidden="true" style="display:block; flex-shrink:0; filter:brightness(0) invert(1);">{{ rgPanelLabel }}</button></sc-if>
4492              <span onClick="{{ resetRosganLot }}" style="font-size:13px; color:#e63946; font-weight:700; cursor:pointer;">Limpiar formulario</span>
4493            </div>
4494          </div>
4495
4496          <sc-if value="{{ rosganLotMessageVisible }}" hint-placeholder-val="{{ false }}"><div style="margin-bottom:16px; padding:13px 16px; border-radius:13px; background:{{ rosganLotMessageBg }}; color:{{ rosganLotMessageColor }}; font-size:13.5px; font-weight:700;">{{ rosganLotMessage }}</div></sc-if>
4497
4498          <sc-if value="{{ rosganLotLoading }}" hint-placeholder-val="{{ false }}">
4499            <div style="display:grid; gap:14px;">
4500              <div style="height:170px; border-radius:22px; background:linear-gradient(90deg,#e5edf5 25%,#f9fbfd 50%,#e5edf5 75%); background-size:900px 100%; animation:shimmer 1.2s linear infinite;"></div>
4501              <div style="height:330px; border-radius:22px; background:linear-gradient(90deg,#e5edf5 25%,#f9fbfd 50%,#e5edf5 75%); background-size:900px 100%; animation:shimmer 1.2s linear infinite;"></div>
4502            </div>
4503          </sc-if>
4504
4505          <sc-if value="{{ rosganLotReady }}" hint-placeholder-val="{{ true }}">
4506            <div style="display:flex; gap:8px; margin-bottom:20px;">
4507              <sc-for list="{{ rgWizardSteps }}" as="w" hint-placeholder-count="3"><div class="rg-wizard-step" onClick="{{ w.go }}" style="flex:1; min-width:0; cursor:pointer;"><div style="height:5px; border-radius:100px; background:{{ w.barBg }}; transition:background 260ms;"></div><div style="display:flex; align-items:center; gap:8px; margin-top:9px;"><span style="width:24px; height:24px; border-radius:50%; display:grid; place-items:center; font-size:12px; font-weight:800; flex-shrink:0; background:{{ w.dotBg }}; color:{{ w.dotColor }}; border:1px solid {{ w.dotBorder }};"><sc-if value="{{ w.done }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if><sc-if value="{{ w.numberVisible }}" hint-placeholder-val="{{ true }}">{{ w.number }}</sc-if></span><div class="rg-step-copy" style="min-width:0;"><div style="font-size:13px; font-weight:700; color:{{ w.titleColor }}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.title }}</div><div style="font-size:11px; color:#8593a6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.text }}</div></div></div></div></sc-for>
4508            </div>
4509            <div class="rg-wizard-layout" style="display:grid; grid-template-columns:{{ rgLayoutCols }}; gap:20px; align-items:start;">
4510              <div class="rg-wizard-panel" style="background:#fff; border-radius:20px; box-shadow:0 12px 40px rgba(0,30,60,.1); padding:clamp(20px,3vw,30px); min-height:470px; display:flex; flex-direction:column;">
4511                <div key="{{ rgStepKey }}" style="animation:pageIn 300ms cubic-bezier(.22,.72,.18,1) both; flex:1;">
4512              <sc-if value="{{ rgIsStep0 }}" hint-placeholder-val="{{ true }}">
4513              <section>
4514                <div class="rg-provenance-section-head"><div><span style="width:34px; height:34px; flex:none; border-radius:11px; background:#e6f1fb; color:#0a5ca8; display:grid; place-items:center; font-size:14px; font-weight:900;">1</span><div><h2 style="margin:0; color:#102f50; font-size:18px;">Remate y procedencia</h2><p style="margin:2px 0 0; color:#718296; font-size:12.5px;">Definí dónde se publica y a quién pertenece el lote.</p></div></div></div>
4515                <div class="rg-provenance-grid" style="display:grid;">
4516                  <div class="rg-provenance-field"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">REMATE *
4517                    <select value="" data-dc-attr-value="{{ rgRemateId }}" onChange="{{ onRgRemate }}" disabled="{{ rgEditingLot }}" style="width:100%; min-height:46px; border:1px solid #ccdae8; border-radius:12px; background:#fff; color:#102f50; padding:0 13px; font:600 13px inherit; outline:none;"><option value="">Seleccionar remate…</option><sc-for list="{{ rosganLotRemates }}" as="r" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ r.id }}">{{ r.name }} · {{ r.meta }}</option></sc-for></select>
4518                  </label></div>
4519                  <div class="rg-cons-picker rg-provenance-field">
4520                    <span class="rg-cons-picker-label">CONSIGNATARIA *</span>
4521                    <button class="rg-cons-picker-button" disabled="{{ rgConsignatariaDisabled }}" type="button" onClick="{{ toggleRgConsignatariaMenu }}" aria-label="Seleccionar consignataria" aria-expanded="{{ rgConsignatariaMenuExpanded }}">
4522                      <span class="rg-cons-picker-logo">
4523                        <sc-if value="{{ rgSelectedConsignatariaHasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ rgSelectedConsignatariaLogo }}" alt=""></sc-if>
4524                        <sc-if value="{{ rgSelectedConsignatariaShowsInitials }}" hint-placeholder-val="{{ true }}">{{ rgSelectedConsignatariaInitials }}</sc-if>
4525                      </span>
4526                      <span class="rg-cons-picker-name">{{ rgSelectedConsignatariaName }}</span>
4527                      <span aria-hidden="true" style="color:#6f8297;font-size:13px;transform:rotate({{ rgConsignatariaChevronRotation }});transition:transform .16s;"><i class="dc-lucide dc-lucide-chevron-down"></i></span>
4528                    </button>
4529                    <sc-if value="{{ rgConsignatariaMenuOpen }}" hint-placeholder-val="{{ false }}">
4530                      <div class="rg-cons-picker-menu" role="listbox" aria-label="Consignatarias">
4531                        <sc-for list="{{ rosganLotConsignatarias }}" as="c" hint-placeholder-count="3">
4532                          <button class="{{ c.className }}" type="button" onClick="{{ c.pick }}" role="option" aria-selected="{{ c.selected }}">
4533                            <span class="rg-cons-picker-logo"><sc-if value="{{ c.hasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ c.logo }}" alt=""></sc-if><sc-if value="{{ c.showsInitials }}" hint-placeholder-val="{{ true }}">{{ c.initials }}</sc-if></span>
4534                            <span class="rg-cons-picker-name">{{ c.name }}</span>
4535                            <sc-if value="{{ c.selected }}" hint-placeholder-val="{{ false }}"><span aria-hidden="true" style="color:#1f7a63;font-size:16px;font-weight:900;"><i class="dc-lucide dc-lucide-check"></i></span></sc-if>
4536                          </button>
4537                        </sc-for>
4538                      </div>
4539                    </sc-if>
4540                  </div>
4541                  <div class="rg-provenance-field">
4542                    <span class="rg-field-label-row" style="display:flex;align-items:center;justify-content:space-between;gap:8px;color:#60758c;font-size:11px;font-weight:800;letter-spacing:.05em;">COMITENTE *<sc-if value="{{ rgCanCreateComitente }}" hint-placeholder-val="false"><button class="rg-provenance-action rg-create-button" type="button" onClick="{{ toggleRgComitenteForm }}" disabled="{{ rgComitenteCreateDisabled }}" style="opacity:{{ rgComitenteCreateOpacity }};"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Crear comitente</button></sc-if></span>
4543                    <div data-rg-comitente-picker style="position:relative; min-width:0;">
4544                      <input id="rg-comitente-search" type="search" value="" data-dc-attr-value="{{ rgComitenteSearch }}" onInput="{{ onRgComitenteSearch }}" onFocus="{{ openRgComitenteMenu }}" onBlur="{{ closeRgComitenteMenu }}" aria-label="Buscar comitente" aria-describedby="confidential-comitente-search" disabled="{{ rgComitenteSearchDisabled }}" autocomplete="off" placeholder="Buscar por alias, razón social o CUIT" style="width:100%; min-height:46px; border:1px solid #ccdae8; border-radius:12px; background:#fff; color:#102f50; padding:0 38px 0 13px; font:600 13px inherit; outline:none;">
4545                      <span aria-hidden="true" style="position:absolute; right:13px; top:14px; color:#718296; pointer-events:none;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span>
4546                      <sc-if value="{{ rgComitenteMenuOpen }}" hint-placeholder-val="{{ false }}">
4547                        <div role="listbox" aria-label="Comitentes" style="position:absolute; z-index:35; left:0; right:0; top:calc(100% + 6px); max-height:230px; overflow:auto; padding:6px; border:1px solid #d4e0ec; border-radius:12px; background:#fff; box-shadow:0 16px 34px rgba(0,38,78,.18);">
4548                          <sc-for list="{{ rgFilteredComitentes }}" as="c" hint-placeholder-count="4"><button type="button" role="option" aria-selected="{{ c.selected }}" onMouseDown="{{ keepRgComitenteMenu }}" onBlur="{{ closeRgComitenteMenu }}" onClick="{{ c.pick }}" style="width:100%; display:grid; gap:2px; border:0; border-radius:9px; background:{{ c.bg }}; color:#102f50; padding:9px 10px; text-align:left; cursor:pointer;"><span style="display:flex;align-items:center;gap:8px;"><strong style="font-size:12.5px;">{{ c.name }}</strong></span><sc-if value="{{ c.hasMeta }}" hint-placeholder-val="{{ false }}"><span style="font-size:10.5px; color:#718296;">{{ c.meta }}</span></sc-if></button></sc-for>
4549                          <sc-if value="{{ rgComitenteHasMore }}"><button type="button" class="rg-est-more" onMouseDown="{{ keepRgComitenteMenu }}" onBlur="{{ closeRgComitenteMenu }}" onClick="{{ moreRgComitentes }}">Mostrar más alias</button></sc-if>
4550                          <sc-if value="{{ rgComitenteNoResults }}" hint-placeholder-val="{{ false }}"><div style="padding:12px 10px; color:#718296; font-size:12px; text-align:center;">No encontramos comitentes con ese nombre.</div></sc-if>
4551                        </div>
4552                      </sc-if>
4553                    </div>
4554                    <small id="confidential-comitente-search" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">&#9888;</span><span>Dato confidencial perteneciente a la firma consignataria</span></small>
4555                    <sc-if value="{{ rgCanCreateComitente }}" hint-placeholder-val="false">
4555<button class="rg-provenance-action" type="button" onClick="{{ openRgLegacyComitentes }}" disabled="{{ rgComitenteCreateDisabled }}" style="margin-top:6px;min-height:32px;">Completar datos fiscales (opcional)</button></sc-if><sc-if value="{{ rgCanEditComitenteAlias }}"><button type="button" class="rg-provenance-action" onClick="{{ editRgComitenteAlias }}" style="margin-top:6px;min-height:32px;">Editar alias</button></sc-if>
4556                  <sc-if value="{{ rgCanCreateComitente }}"><button type="button" class="rg-provenance-action" onClick="{{ openRgComitenteMerge }}" disabled="{{ rgComitenteCreateDisabled }}" style="margin-top:6px;min-height:32px;">Fusionar comitentes</button></sc-if>
4557                                  <sc-if value="{{ rgComitenteFormOpen }}" hint-placeholder-val="{{ false }}">
4558                  <div id="rg-inline-comitente" class="rg-inline-create" style="margin-top:16px; padding:17px 18px; border:1px solid #dbe7f2; border-radius:14px; background:#f6f9fc;">
4559                    <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:13px;"><div><div style="color:#102f50; font-size:15px; font-weight:850;">Nuevo comitente</div><div style="margin-top:3px; color:#718296; font-size:11.5px;">Primero creá el comitente; después vas a asociarle un establecimiento.</div></div><button type="button" onClick="{{ toggleRgComitenteForm }}" aria-label="Cerrar alta de comitente" style="width:30px; height:30px; border:1px solid #d5e1ec; border-radius:9px; background:#fff; color:#65758a; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div>
4560                    <div class="rg-form-grid" style="display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px;">
4561                      <label style="display:grid;gap:6px;color:#60758c;font-size:12px;font-weight:700;">ALIAS *<input required type="text" value="" data-dc-attr-value="{{ rgComitenteAlias }}" onInput="{{ onRgComitenteAlias }}" placeholder="Ej.: Estancia El Ombú" style="box-sizing:border-box;min-width:0;width:100%;height:43px;border:1px solid #ccdae8;border-radius:11px;padding:0 12px;font:inherit;background:#fff;"></label>
4562                      <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">CUIT (opcional)<span style="position:relative; display:block;"><input aria-describedby="confidential-rgComitenteCuit" inputmode="numeric" maxlength="11" value="" data-dc-attr-value="{{ rgComitenteCuit }}" onInput="{{ onRgComitenteCuit }}" placeholder="11 dígitos" style="box-sizing:border-box; width:100%; height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 42px 0 12px; background:#fff;"><sc-if value="{{ rgComitenteLookupLoading }}" hint-placeholder-val="false"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute; right:13px; top:13px; color:#0a5ca8;"></i></sc-if></span><small id="confidential-rgComitenteCuit" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">&#9888;</span><span>Dato confidencial perteneciente a la firma consignataria</span></small></label>
4563<label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">RAZÓN SOCIAL (opcional)<input value="" data-dc-attr-value="{{ rgComitenteFiscalName }}" readonly="true" placeholder="Se completa automáticamente desde ARCA" style="height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 12px; background:#eef2f6;"></label>
4564                      <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">PROVINCIA FISCAL (opcional)<input value="" data-dc-attr-value="{{ rgComitenteProvince }}" readonly="true" placeholder="Se completa desde ARCA" style="height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 12px; background:#eef2f6;"></label>
4565                      <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">LOCALIDAD FISCAL (opcional)<input value="" data-dc-attr-value="{{ rgComitenteLocality }}" readonly="true" placeholder="Se completa desde ARCA" style="height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 12px; background:#eef2f6;"></label>
4566                    </div>
4567                    <sc-if value="{{ rgComitenteMessageVisible }}" hint-placeholder-val="{{ false }}"><div style="margin-top:11px; padding:9px 11px; border-radius:9px; background:{{ rgComitenteMessageBg }}; color:{{ rgComitenteMessageColor }}; font-size:12px; font-weight:700;">{{ rgComitenteMessage }}</div></sc-if>
4568                    <div style="display:flex; justify-content:flex-end; gap:9px; margin-top:14px;"><button type="button" onClick="{{ toggleRgComitenteForm }}" disabled="{{ rgComitenteBusy }}" style="min-height:39px; padding:0 16px; border:1px solid #d5e0eb; border-radius:10px; background:#fff; color:#526276; font:750 12px inherit; cursor:pointer;">Cancelar</button><button type="button" onClick="{{ saveRgComitente }}" disabled="{{ rgComitenteSaveDisabled }}" style="min-height:39px; padding:0 18px; border:0; border-radius:10px; background:#1f7a63; color:#fff; font:800 12px inherit; cursor:{{ rgComitenteSaveCursor }}; opacity:{{ rgComitenteSaveOpacity }};">{{ rgComitenteSaveLabel }}</button></div>
4569                  </div>
4570                </sc-if>
4571                  </div>
4572                  <div class="rg-provenance-field">
4573                    <span class="rg-field-label-row" style="display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">ESTABLECIMIENTO *<sc-if value="{{ rgCanCreateEstablishment }}" hint-placeholder-val="false"><button class="rg-provenance-action rg-create-button" type="button" onClick="{{ toggleRgEstablishmentForm }}" disabled="{{ rgEstablishmentDisabled }}" style="opacity:{{ rgComitenteCreateOpacity }};"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Crear establecimiento</button></sc-if></span>
4574                    <div class="rg-est-picker" onKeyDown="{{ onRgEstablishmentPickerKey }}" onBlur="{{ blurRgEstablishmentPicker }}">
4575                      <button class="rg-est-trigger" type="button" aria-label="Seleccionar establecimiento" aria-haspopup="listbox" aria-expanded="{{ rgEstablishmentMenuOpen }}" aria-controls="rg-establishment-options" onClick="{{ toggleRgEstablishmentMenu }}" disabled="{{ rgEstablishmentDisabled }}">
4576
4577                        <span class="rg-est-label">{{ rgEstablishmentButtonLabel }}</span><i class="dc-lucide dc-lucide-chevron-down" aria-hidden="true"></i>
4578                      </button>
4579                      <sc-if value="{{ rgEstablishmentMenuOpen }}" hint-placeholder-val="false">
4580                        <div class="rg-est-menu">
4581                          <input type="search" class="rg-est-search" aria-label="Buscar establecimiento" placeholder="Buscar nombre o localidad…" value="" data-dc-attr-value="{{ rgEstablishmentSearch }}" onInput="{{ onRgEstablishmentSearch }}" autocomplete="off">
4582                          <div id="rg-establishment-options" role="listbox" aria-label="Establecimientos" class="rg-est-options">
4583                            <sc-for list="{{ rgEstablishmentOptions }}" as="e" hint-placeholder-count="3"><button type="button" class="rg-est-option" data-rg-est-choice="true" role="option" aria-selected="{{ e.selected }}" onClick="{{ e.pick }}"><span class="rg-est-option-text"><strong>{{ e.name }}</strong><small>{{ e.meta }}</small></span></button></sc-for>
4584                          </div>
4585                          <sc-if value="{{ rgEstablishmentNoMatches }}" hint-placeholder-val="false"><p class="rg-est-count">No encontramos establecimientos.</p></sc-if>
4586                          <sc-if value="{{ rgEstablishmentHasMore }}" hint-placeholder-val="false"><button type="button" class="rg-est-more" onClick="{{ moreRgEstablishments }}">Mostrar más establecimientos</button></sc-if>
4587                          <p class="rg-est-count">{{ rgEstablishmentCountLabel }}</p>
4588                        </div>
4589                      </sc-if>
4590                    </div>
4591                                  <sc-if value="{{ rgEstablishmentFormOpen }}" hint-placeholder-val="{{ false }}">
4592                  <div id="rg-inline-establishment" class="rg-inline-create" style="margin-top:16px; padding:17px 18px; border:1px solid #bcd9cf; border-radius:14px; background:#f1faf7;">
4593                    <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:13px;"><div><div style="color:#102f50; font-size:15px; font-weight:850;">Nuevo establecimiento</div><div style="margin-top:3px; color:#5f766f; font-size:11.5px;">Quedará asociado al comitente seleccionado.</div></div><button type="button" onClick="{{ toggleRgEstablishmentForm }}" aria-label="Cerrar alta de establecimiento" style="width:30px; height:30px; border:1px solid #cbded8; border-radius:9px; background:#fff; color:#65758a; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div>
4594                    <div class="rg-form-grid" style="display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px;">
4595                      <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">NOMBRE DEL ESTABLECIMIENTO *<input value="" data-dc-attr-value="{{ rgEstablishmentName }}" onInput="{{ onRgEstablishmentName }}" placeholder="Ej. Campo La Prueba" style="height:43px; border:1px solid #c8ddd6; border-radius:11px; padding:0 12px; background:#fff;"></label>
4596                      <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">RENSPA <span style="font-weight:500; color:#8a9aac;">Opcional</span><input value="" data-dc-attr-value="{{ rgEstablishmentRenspa }}" onInput="{{ onRgEstablishmentRenspa }}" placeholder="Podés dejarlo vacío" style="height:43px; border:1px solid #c8ddd6; border-radius:11px; padding:0 12px; background:#fff;"></label>
4597                      <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">PROVINCIA *<select value="" data-dc-attr-value="{{ rgEstablishmentProvince }}" onChange="{{ onRgEstablishmentProvince }}" style="height:43px; border:1px solid #c8ddd6; border-radius:11px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgEstablishmentProvinceOptions }}" as="p" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for></select></label>
4598                      <label style="position:relative;display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;"><span style="display:flex;align-items:center;justify-content:space-between;"><span>LOCALIDAD *</span><small style="display:inline-flex;align-items:center;gap:4px;color:#718296;font-weight:650;text-transform:none;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;object-fit:contain;">Google</small></span><input value="" data-dc-attr-value="{{ rgEstablishmentLocality }}" onInput="{{ onRgEstablishmentLocality }}" onFocus="{{ openRgEstablishmentPlaces }}" onBlur="{{ closeRgEstablishmentPlaces }}" disabled="{{ rgEstablishmentLocalityDisabled }}" autocomplete="off" placeholder="Escribí una localidad" style="height:43px;border:1px solid #c8ddd6;border-radius:11px;padding:0 38px 0 12px;background:{{ rgEstablishmentLocalityBg }};"><sc-if value="{{ rgEstablishmentPlaceLoading }}" hint-placeholder-val="false"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:13px;top:35px;color:#0a5ca8;"></i></sc-if><sc-if value="{{ rgEstablishmentPlacesOpen }}" hint-placeholder-val="false"><div style="position:absolute;z-index:45;left:0;right:0;top:calc(100% + 5px);padding:5px;border:1px solid #cbded8;border-radius:11px;background:#fff;box-shadow:0 14px 30px rgba(0,35,70,.18);max-height:220px;overflow:auto;"><sc-for list="{{ rgEstablishmentPlaces }}" as="p" hint-placeholder-count="4"><button type="button" onClick="{{ p.pick }}" style="width:100%;display:grid;gap:2px;border:0;border-radius:8px;background:#fff;padding:8px 9px;text-align:left;cursor:pointer;"><strong style="font-size:11.5px;color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px;color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if></label>
4599                    </div>
4600                    <sc-if value="{{ rgEstablishmentPlaceError }}" hint-placeholder-val="false"><div style="margin-top:9px;color:#8a5a00;font-size:10.5px;">{{ rgEstablishmentPlaceError }}</div></sc-if>
4601                    <sc-if value="{{ rgEstablishmentMessageVisible }}" hint-placeholder-val="{{ false }}"><div style="margin-top:11px; padding:9px 11px; border-radius:9px; background:{{ rgEstablishmentMessageBg }}; color:{{ rgEstablishmentMessageColor }}; font-size:12px; font-weight:700;">{{ rgEstablishmentMessage }}</div></sc-if>
4602                    <div style="display:flex; justify-content:flex-end; gap:9px; margin-top:14px;"><button type="button" onClick="{{ toggleRgEstablishmentForm }}" disabled="{{ rgEstablishmentSaveDisabled }}" style="min-height:39px; padding:0 16px; border:1px solid #d5e0eb; border-radius:10px; background:#fff; color:#526276; font:750 12px inherit; cursor:pointer;">Cancelar</button><button type="button" onClick="{{ saveRgEstablishment }}" disabled="{{ rgEstablishmentSaveDisabled }}" style="min-height:39px; padding:0 18px; border:0; border-radius:10px; background:#1f7a63; color:#fff; font:800 12px inherit; cursor:pointer;">{{ rgEstablishmentSaveLabel }}</button></div>
4603                  </div>
4604                </sc-if>
4605                  </div>
4606                  <div class="rg-provenance-optional"><div class="rg-provenance-field"><label id="rg-commercial-label">
4606RESPONSABLE COMERCIAL (OPCIONAL)</label>
4607                    <div style="position:relative;" onKeyDown="{{ onRgCertifierKey }}">
4608                      <button type="button" aria-labelledby="rg-commercial-label rg-commercial-value" aria-expanded="{{ rgCertifierMenuOpen }}" aria-controls="rg-commercial-options" onClick="{{ toggleRgCertifierMenu }}" disabled="{{ rgCertifierDisabled }}" style="width:100%;min-height:46px;border:1px solid #ccdae8;border-radius:12px;padding:8px 13px;background:#fff;color:#102f50;display:flex;align-items:center;gap:9px;text-align:left;font:13px inherit;cursor:pointer;">
4609                        <sc-if value="{{ rgSelectedCertifier.hasAvatar }}"><span style="display:grid;place-items:center;width:28px;height:28px;border-radius:50%;overflow:hidden;flex:0 0 28px;background:{{ rgSelectedCertifier.bg }};"><img data-dc-attr-src="{{ rgSelectedCertifier.avatar }}" onError="{{ rgSelectedCertifier.avatarError }}" alt="" style="width:{{ rgSelectedCertifier.size }};height:{{ rgSelectedCertifier.size }};object-fit:{{ rgSelectedCertifier.fit }};"></span></sc-if>
4610                        <span id="rg-commercial-value" style="flex:1;">{{ rgSelectedCertifier.label }}</span><span aria-hidden="true">⌄</span>
4611                      </button>
4612                      <sc-if value="{{ rgCertifierMenuOpen }}"><div id="rg-commercial-options" role="group" aria-labelledby="rg-commercial-label" style="position:absolute;top:calc(100% + 5px);left:0;right:0;z-index:30;max-height:240px;overflow:auto;background:white;border:1px solid #d7e2ed;border-radius:12px;padding:5px;box-shadow:0 12px 30px #002e5f22;">
4613                        <sc-for list="{{ rgCertifierOptions }}" as="certifier"><button type="button" onClick="{{ certifier.pick }}" aria-pressed="{{ certifier.selected }}" style="display:flex;align-items:center;gap:9px;width:100%;border:0;border-radius:8px;padding:9px;background:{{ certifier.rowBg }};color:#102f50;text-align:left;font:13px inherit;cursor:pointer;">
4614                          <span style="display:grid;place-items:center;width:28px;height:28px;border-radius:50%;overflow:hidden;flex:0 0 28px;background:{{ certifier.bg }};"><sc-if value="{{ certifier.hasAvatar }}"><img data-dc-attr-src="{{ certifier.avatar }}" onError="{{ certifier.avatarError }}" alt="" style="width:{{ certifier.size }};height:{{ certifier.size }};object-fit:{{ certifier.fit }};"></sc-if><sc-if value="{{ certifier.noAvatar }}"><span>{{ certifier.initial }}</span></sc-if></span>
4615                          <span style="flex:1;">{{ certifier.label }}</span><span aria-hidden="true">{{ certifier.check }}</span>
4616                        </button></sc-for>
4617                      </div></sc-if>
4618                    </div>
4619                  <span class="rg-provenance-help" role="status">{{ rgCertifierStatus }}</span></div></div>
4620                </div>
4621
4622
4623                <sc-if value="{{ rgRelationsLoading }}" hint-placeholder-val="{{ false }}"><div style="margin-top:11px; color:#0a5ca8; font-size:12px; font-weight:700;">Cargando comitentes y establecimientos…</div></sc-if>
4624              </section>
4625              </sc-if>
4626
4627              <sc-if value="{{ rgIsStep1 }}" hint-placeholder-val="{{ false }}">
4628              <section>
4629                <div style="display:flex; align-items:center; gap:12px; margin-bottom:19px;"><span style="width:34px; height:34px; border-radius:11px; background:#e6f4ef; color:#1f7a63; display:grid; place-items:center; font-size:14px; font-weight:900;">2</span><div><h2 style="margin:0; color:#102f50; font-size:18px;">Categoría, raza y cantidad</h2><p style="margin:2px 0 0; color:#718296; font-size:12.5px;">La categoría activa automáticamente el formulario correspondiente.</p></div></div>
4630                <div class="rg-lot-type-choices" role="group" aria-label="Tipo de lote">
4631                  <sc-for list="{{ rgLotTypeOptions }}" as="t" hint-placeholder-count="3"><button type="button" class="rg-lot-type-choice {{ t.className }}" onClick="{{ t.pick }}" aria-pressed="{{ t.selected }}" style="--rg-type-bg:{{ t.bg }};--rg-type-color:{{ t.color }};--rg-type-border:{{ t.border }};">{{ t.label }}</button></sc-for>
4632                </div>
4633                <div class="rg-form-grid rg-category-quantity-grid" style="display:grid; grid-template-columns:minmax(240px,1.15fr) minmax(210px,.85fr); gap:14px; align-items:start;">
4634                  <div style="display:grid; gap:10px; align-content:start; min-width:0;">
4635                    <label style="display:grid; gap:6px; min-width:0; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">CATEGORÍA *
4636                      <select value="" data-dc-attr-value="{{ rgCategoryId }}" onChange="{{ onRgCategory }}" disabled="{{ rgCategoryDisabled }}" style="width:100%; min-width:0; min-height:46px; border:1px solid #ccdae8; border-radius:12px; background:{{ rgCategoryBg }}; color:#102f50; padding:0 13px; font:600 13px inherit; outline:none;"><option value="">{{ rgCategoryPlaceholder }}</option><sc-for list="{{ rosganLotCategories }}" as="c" hint-placeholder-count="8"><option value="" data-dc-attr-value="{{ c.id }}">{{ c.name }}</option></sc-for></select>
4637                    </label>
4638                    <sc-if value="{{ rgAllowsTermino }}" hint-placeholder-val="{{ false }}"><div class="rg-state-toggles" style="margin:0;"><button type="button" class="rg-state-toggle" onClick="{{ toggleRgTermino }}" aria-pressed="{{ rgTerminoPressed }}" style="color:{{ rgTerminoColor }};background:{{ rgTerminoBg }};border-color:{{ rgTerminoBorder }};box-shadow:{{ rgTerminoShadow }};"><img src="assets/icons/lucide/clock-3.svg" alt="" aria-hidden="true" width="21" height="21" style="width:21px;height:21px;filter:{{ rgTerminoIconFilter }};opacity:{{ rgTerminoIconOpacity }};"><span>Venta a Término</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgTerminoCheckBg }};color:{{ rgTerminoCheckColor }};">{{ rgTerminoCheck }}</span></button></div></sc-if>
4639                  </div>
4640                  <sc-if value="{{ rgIsTotalQuantity }}" hint-placeholder-val="{{ true }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">CANTIDAD *<input type="number" min="1" value="" data-dc-attr-value="{{ rgQuantity }}" onInput="{{ onRgQuantity }}" placeholder="Ej. 45" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 13px; font:700 14px inherit; color:#102f50; outline:none;"></label></sc-if>
4641                  <sc-if value="{{ rgIsMaleFemale }}" hint-placeholder-val="{{ false }}"><div class="rg-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">MACHOS *<input type="number" min="0" value="" data-dc-attr-value="{{ rgMaleQuantity }}" onInput="{{ onRgMaleQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">HEMBRAS *<input type="number" min="0" value="" data-dc-attr-value="{{ rgFemaleQuantity }}" onInput="{{ onRgFemaleQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label></div></sc-if>
4642                  <sc-if value="{{ rgIsMothersCalves }}" hint-placeholder-val="{{ false }}"><div class="rg-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">MADRES *<input type="number" min="1" value="" data-dc-attr-value="{{ rgMotherQuantity }}" onInput="{{ onRgMotherQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">CRÍAS *<input type="number" min="1" value="" data-dc-attr-value="{{ rgCalfQuantity }}" onInput="{{ onRgCalfQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label></div></sc-if>
4643                </div>
4644                <div style="margin-top:18px; display:grid; gap:10px;">
4645                  <div style="display:flex; align-items:center; justify-content:space-between; gap:10px;"><div style="font-size:12.5px; font-weight:750; color:#3d4c5f;">Razas</div><span style="color:#78899d; font-size:11.5px; font-weight:650;">Máximo 3 · {{ rgBreedsCount }}
4645/3</span></div>
4646                  <label style="position:relative; display:block;"><span style="position:absolute; left:14px; top:50%; transform:translateY(-50%); color:#6f8297; pointer-events:none; font-size:15px;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span><input type="search" aria-label="Buscar raza" value="" data-dc-attr-value="{{ rgBreedSearch }}" onInput="{{ onRgBreedSearch }}" placeholder="Empezá a escribir la raza…" autocomplete="off" style="width:100%; height:46px; border:1px solid #c8d8e8; border-radius:13px; background:#fff; padding:0 42px; color:#102f50; font:650 13px inherit; outline:none; box-shadow:0 3px 10px rgba(14,67,113,.04);"></label>
4647                  <sc-if value="{{ rgBreedSearchHint }}" hint-placeholder-val="{{ true }}"><div style="font-size:11.5px; line-height:1.45; color:#7b8da0;">Empezá a escribir para buscar una raza.</div></sc-if>
4648                  <sc-if value="{{ rgBreedSearchHasResults }}" hint-placeholder-val="{{ true }}"><div style="display:flex; flex-wrap:wrap; gap:8px;"><sc-for list="{{ rgBreedSearchResults }}" as="b" hint-placeholder-count="5"><button type="button" onClick="{{ b.pick }}" style="display:inline-flex; align-items:center; gap:7px; min-height:38px; padding:2px 13px 2px 7px; border-radius:999px; border:1px solid {{ b.border }}; background:{{ b.bg }}; color:{{ b.color }}; font:750 12px inherit; cursor:pointer; transition:transform 140ms,box-shadow 140ms;"><sc-if value="{{ b.hasIcon }}" hint-placeholder-val="{{ false }}"><img src="" data-dc-attr-src="{{ b.icon }}" alt="" style="width:30px; height:30px; object-fit:contain;"></sc-if><sc-if value="{{ b.hasColors }}" hint-placeholder-val="{{ false }}"><span aria-hidden="true" style="display:inline-flex; width:18px; height:10px; overflow:hidden; border-radius:999px; box-shadow:0 0 0 1px rgba(9,30,52,.12);"><i style="display:block; width:50%; background:#17191d;"></i><i style="display:block; width:50%; background:#a84735;"></i></span></sc-if>{{ b.name }}</button></sc-for></div></sc-if>
4649                  <sc-if value="{{ rgBreedNoResults }}" hint-placeholder-val="{{ false }}"><div style="padding:10px 12px; border-radius:11px; background:#f6f8fb; color:#6e7f92; font-size:12px;">No encontramos una raza con ese nombre.</div></sc-if>
4650                  <sc-if value="{{ rgHasSelectedBreedFamilies }}" hint-placeholder-val="{{ false }}"><div style="display:grid; gap:8px; padding-top:3px;"><span style="font-size:10.5px; font-weight:850; color:#7a8ca0; letter-spacing:.08em; text-transform:uppercase;">Seleccionadas</span><div style="display:grid; gap:8px;"><sc-for list="{{ rgSelectedBreedFamilies }}" as="b" hint-placeholder-count="3"><div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; min-height:54px; padding:7px 9px 7px 12px; border:1px solid #d7e3ed; border-radius:14px; background:linear-gradient(135deg,#f8fbfd,#eef4f8);"><span style="display:inline-flex; align-items:center; min-height:39px; overflow:hidden; border:1px solid #8eb9df; border-radius:999px; background:#eaf4ff; color:#07589d; box-shadow:0 3px 9px rgba(10,92,168,.07);"><span style="display:inline-flex; align-items:center; gap:5px; height:38px; padding:0 5px; font-size:12px; font-weight:800;"><sc-if value="{{ b.hasIcon }}" hint-placeholder-val="{{ false }}"><img src="" data-dc-attr-src="{{ b.icon }}" alt="" style="width:32px; height:32px; object-fit:contain;"></sc-if><sc-if value="{{ b.showBlack }}" hint-placeholder-val="{{ false }}"><i aria-label="Negros seleccionados" style="display:block; width:10px; height:10px; border-radius:50%; background:#17191d; box-shadow:0 0 0 1px rgba(9,30,52,.16);"></i></sc-if><sc-if value="{{ b.showRed }}" hint-placeholder-val="{{ false }}"><i aria-label="Colorados seleccionados" style="display:block; width:10px; height:10px; border-radius:50%; background:#a84735; box-shadow:0 0 0 1px rgba(109,37,24,.16);"></i></sc-if>{{ b.name }}</span><button type="button" onClick="{{ b.remove }}" aria-label="Quitar raza" style="width:31px; height:38px; border:0; background:transparent; color:#3d78a9; font:800 17px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></span><div style="display:flex; flex:1; align-items:center; justify-content:flex-start; gap:10px; flex-wrap:wrap;"><sc-if value="{{ b.hasColors }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; gap:7px; flex-wrap:wrap; margin-left:auto; padding:4px 8px; border-radius:10px; background:rgba(255,255,255,.62);"><span style="font-size:10.5px; font-weight:800; color:#718397; text-transform:uppercase; letter-spacing:.05em;">Color</span><sc-if value="{{ b.blackAvailable }}" hint-placeholder-val="{{ true }}"><button type="button" onClick="{{ b.toggleBlack }}" style="display:inline-flex;align-items:center;gap:6px;min-height:34px; padding:0 13px; border-radius:999px; border:1px solid {{ b.blackBorder }}; background:{{ b.blackBg }}; color:{{ b.blackColor }}; font:800 11.5px inherit; cursor:pointer;"><i aria-hidden="true" style="width:8px;height:8px;border-radius:50%;background:currentColor;"></i>Negros</button></sc-if><sc-if value="{{ b.redAvailable }}" hint-placeholder-val="{{ true }}"><button type="button" onClick="{{ b.toggleRed }}" style="display:inline-flex;align-items:center;gap:6px;min-height:34px; padding:0 13px; border-radius:999px; border:1px solid {{ b.redBorder }}; background:{{ b.redBg }}; color:{{ b.redColor }}; font:800 11.5px inherit; cursor:pointer;"><i aria-hidden="true" style="width:8px;height:8px;border-radius:50%;background:currentColor;"></i>Colorados</button></sc-if></div></sc-if><div style="display:flex; order:-1; align-items:center; gap:7px; padding:4px;"><span style="font-size:10.5px; font-weight:800; color:#718397; text-transform:uppercase; letter-spacing:.05em;">Cruzas</span><button type="button" onClick="{{ b.toggleCrosses }}" style="display:inline-flex;align-items:center;gap:5px;min-height:34px; padding:0 14px; border-radius:999px; border:1px solid {{ b.crossesBorder }}; background:{{ b.crossesBg }}; color:{{ b.crossesColor }}; font:800 11.5px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i>Y sus cruzas</button></div></div></div></sc-for></div></div></sc-if>
4651                </div>
4652              </section>
4653              </sc-if>
4654
4655              <sc-if value="{{ rgIsStep2 }}" hint-placeholder-val="{{ false }}">
4656                <section style="background:#fff; border:1px solid #dce7f2; border-radius:22px; padding:clamp(18px,3vw,28px); box-shadow:0 12px 34px rgba(0,39,82,.07);">
4657                  <div style="display:flex; align-items:center; gap:12px; margin-bottom:19px;"><span style="width:34px; height:34px; border-radius:11px; background:#fff0dd; color:#a86300; display:grid; place-items:center; font-size:14px; font-weight:900;">3</span><div><h2 style="margin:0; color:#102f50; font-size:18px;">Características del lote</h2><p style="margin:2px 0 0; color:#718296; font-size:12.5px;">Solo se muestran los datos compatibles con la categoría elegida.</p></div></div>
4658                  <div class="rg-state-toggles">
4659                    <sc-if value="{{ rgAllowsAptoChina }}" hint-placeholder-val="{{ false }}"><button type="button" class="rg-state-toggle" onClick="{{ toggleRgChina }}" aria-pressed="{{ rgChinaPressed }}" style="color:{{ rgChinaColor }};background:{{ rgChinaBg }};border-color:{{ rgChinaBorder }};box-shadow:{{ rgChinaShadow }};"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="23" height="16" style="width:23px;height:16px;border-radius:3px;filter:{{ rgChinaFlagFilter }};opacity:{{ rgChinaFlagOpacity }};"><span>Apto China</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgChinaCheckBg }};color:{{ rgChinaCheckColor }};">{{ rgChinaCheck }}</span></button></sc-if>
4660                    <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid;gap:6px;font-size:11px;font-weight:800;color:#60758c;">MÍO-MÍO *<select required aria-required="true" value="" data-dc-attr-value="{{ rgMioValue }}" onChange="{{ onRgMio }}" style="height:44px;min-width:170px;border:1px solid #ccdae8;border-radius:12px;padding:0 12px;background:#fff;"><option value="">Seleccionar…</option><option value="yes">Sí</option><option value="no">No</option></select></label></sc-if>
4661                  </div>
4662                  <div class="rg-characteristics-grid">
4663                    <sc-if value="{{ rgAllowsAge }}" hint-placeholder-val="{{ false }}"><div class="rg-age-fields"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">EDAD *<input required aria-required="true" type="number" min="1" value="" data-dc-attr-value="{{ rgAgeValue }}" onInput="{{ onRgAgeValue }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">UNIDAD<select value="" data-dc-attr-value="{{ rgAgeUnit }}" onChange="{{ onRgAgeUnit }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 9px; background:#fff;"><sc-for list="{{ rgAgeUnitOptions }}" as="o" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label></div></sc-if>
4664                    <sc-if value="{{ rgAllowsDentition }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">DENTICIÓN<sc-if value="{{ rgHasDentitionOptions }}" hint-placeholder-val="{{ false }}"><select value="" data-dc-attr-value="{{ rgDentition }}" onChange="{{ onRgDentition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgDentitionOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoDentitionOptions }}" hint-placeholder-val="{{ true }}"><input value="" data-dc-attr-value="{{ rgDentition }}" onInput="{{ onRgDentition }}" placeholder="Ej. 4 dientes" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4665                    <sc-if value="{{ rgAllowsReproductiveStatus }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">ESTADO REPRODUCTIVO<sc-if value="{{ rgHasReproductiveStatusOptions }}" hint-placeholder-val="{{ false }}"><select value="" data-dc-attr-value="{{ rgReproductiveStatus }}" onChange="{{ onRgReproductiveStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgReproductiveStatusOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoReproductiveStatusOptions }}" hint-placeholder-val="{{ true }}"><input value="" data-dc-attr-value="{{ rgReproductiveStatus }}" onInput="{{ onRgReproductiveStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4666                    <sc-if value="{{ rgAllowsDestination }}" hint-placeholder-val="{{ false }}"><div style="grid-column:1/-1;"><div style="font-size:11px;font-weight:800;color:#60758c;margin-bottom:8px;">{{ rgDestinationLabel }}</div><div style="display:flex;gap:8px;flex-wrap:wrap;" role="group" aria-label="Destino"><sc-for list="{{ rgDestinationChoices }}" as="o" hint-placeholder-count="4"><button type="button" onClick="{{ o.pick }}" aria-pressed="{{ o.selected }}" style="padding:9px 16px;border-radius:100px;font:600 13px inherit;cursor:pointer;border:1px solid {{ o.border }};background:{{ o.bg }};color:{{ o.color }};">{{ o.label }}</button></sc-for></div></div></sc-if>
4667                    <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">ESTADO *<sc-if value="{{ rgHasBodyConditionOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgBodyCondition }}" onChange="{{ onRgBodyCondition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgBodyConditionOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoBodyConditionOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgBodyCondition }}" onInput="{{ onRgBodyCondition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4668                    <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">CLASE *<sc-if value="{{ rgHasAnimalClassOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgAnimalClass }}" onChange="{{ onRgAnimalClass }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgAnimalClassOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoAnimalClassOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgAnimalClass }}" onInput="{{ onRgAnimalClass }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4669                    <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">GARRAPATA *<sc-if value="{{ rgHasTickStatusOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgTickStatus }}" onChange="{{ onRgTickStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgTickStatusOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoTickStatusOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgTickStatus }}" onInput="{{ onRgTickStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4670                    <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">SANIDAD *<sc-if value="{{ rgHasHealthOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgHealth }}" onChange="{{ onRgHealth }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgHealthOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoHealthOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgHealth }}" onInput="{{ onRgHealth }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4671                    <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">NUTRICIÓN *<sc-if value="{{ rgHasNutritionOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-
4671attr-value="{{ rgNutrition }}" onChange="{{ onRgNutrition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rgNutritionOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoNutritionOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgNutrition }}" onInput="{{ onRgNutrition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if>
4672                    <sc-if value="{{ rgShowConfinement }}" hint-placeholder-val="{{ false }}"><label style="display:grid;gap:6px;font-size:11px;font-weight:800;color:#60758c;">DÍAS DE ENCIERRE<input type="number" min="0" step="1" value="" data-dc-attr-value="{{ rgDaysOfConfinement }}" onInput="{{ onRgDaysOfConfinement }}" style="height:44px;border:1px solid #ccdae8;border-radius:12px;padding:0 12px;"></label></sc-if>
4673                  </div>
4674                  <fieldset style="margin:14px 0 0;padding:14px;border:1px solid #ccdae8;border-radius:12px;">
4675                    <legend style="font-size:11px;font-weight:800;color:#60758c;">PLAZO DE PAGO</legend>
4676                    <div role="group" aria-label="Días del plazo de pago" style="display:flex;flex-wrap:wrap;gap:8px;">
4677                      <sc-for list="{{ rgPaymentDays }}" as="day"><button type="button" class="rg-payment-chip" aria-pressed="{{ day.checked }}" onClick="{{ day.toggle }}"><span class="rg-payment-chip-mark" aria-hidden="true">✓</span>{{ day.label }}</button></sc-for>
4678                    </div>
4679                    <sc-if value="{{ rgOtherPaymentDay }}"><label style="display:grid;gap:6px;margin-top:12px;">Otros días<input aria-label="Otros días de pago" value="" data-dc-attr-value="{{ rgOtherPaymentDayValue }}" onInput="{{ onRgOtherPaymentDay }}" placeholder="Ej.: 45 o 15 y 45" style="min-height:44px;border:1px solid #ccdae8;border-radius:10px;padding:0 12px;"></label></sc-if>
4680                    <label style="display:grid;gap:6px;margin-top:12px;">Referencia del plazo<select aria-label="Referencia del plazo" value="" data-dc-attr-value="{{ rgPaymentReference }}" onChange="{{ onRgPaymentReference }}" style="min-height:44px;border:1px solid #ccdae8;border-radius:10px;padding:0 12px;background:white;"><option value="">Seleccionar…</option><option value="del remate">Del remate</option><option value="de la carga">De la carga</option><option value="other">Otro</option></select></label>
4681                    <sc-if value="{{ rgOtherPaymentReference }}"><label style="display:grid;gap:6px;margin-top:12px;">Otra referencia<input aria-label="Otra referencia del plazo" value="" data-dc-attr-value="{{ rgOtherPaymentReferenceValue }}" onInput="{{ onRgOtherPaymentReference }}" placeholder="Ej.: de la entrega" style="min-height:44px;border:1px solid #ccdae8;border-radius:10px;padding:0 12px;"></label></sc-if>
4682                    <p style="margin:10px 0 0;color:#60758c;font-size:13px;">{{ rgPreviewPaymentTerm }}</p>
4683                  </fieldset>
4684                  <sc-if value="{{ rgHasDescriptionOptions }}" hint-placeholder-val="{{ false }}"><div style="margin-top:14px;"><div style="font-size:11px;font-weight:800;color:#60758c;letter-spacing:.05em;">CARACTERÍSTICAS</div><div style="margin-top:3px;font-size:11.5px;color:#8a9aac;">Seleccioná todas las que correspondan.</div><div class="rg-description-options" role="group" aria-label="Características del lote"><sc-for list="{{ rgDescriptionOptions }}" as="o" hint-placeholder-count="5"><button type="button" class="rg-description-choice" onClick="{{ o.pick }}" aria-pressed="{{ o.selected }}" style="border-color:{{ o.border }};background:{{ o.bg }};color:{{ o.color }};box-shadow:{{ o.shadow }};"><span class="rg-description-choice-mark" aria-hidden="true" style="background:{{ o.markBg }};color:{{ o.markColor }};">{{ o.mark }}</span>{{ o.label }}</button></sc-for></div></div></sc-if>
4685                  <label style="display:grid; gap:6px; margin-top:15px; font-size:11px; font-weight:800; color:#60758c;">OBSERVACIONES ADICIONALES <span style="font-weight:500; color:#8a9aac;">Texto libre opcional.</span><textarea id="rg-lot-description" name="rosganLotDescription" value="" data-dc-attr-value="{{ rgDescription }}" onInput="{{ onRgDescription }}" onChange="{{ onRgDescription }}" onBlur="{{ onRgDescription }}" rows="4" placeholder="Estado general, manejo u otra observación…" style="resize:vertical; border:1px solid #ccdae8; border-radius:12px; padding:12px; font:500 13px/1.5 inherit; color:#102f50;"></textarea></label>
4686                </section>
4687
4688                <section class="rg-specific-panel">
4689                  <div style="font-size:11px; letter-spacing:.14em; color:#8bd9d4; font-weight:800;">DATOS ESPECÍFICOS</div>
4690                  <sc-if value="{{ rgIsInvernada }}" hint-placeholder-val="{{ false }}"><h2 style="font-size:21px; margin:5px 0 17px;">Invernada</h2></sc-if>
4691                  <sc-if value="{{ rgIsVientres }}" hint-placeholder-val="{{ false }}"><h2 style="font-size:21px; margin:5px 0 17px;">Vientres</h2></sc-if>
4692                  <sc-if value="{{ rgIsFaena }}" hint-placeholder-val="{{ false }}"><h2 style="font-size:21px; margin:5px 0 17px;">Faena</h2></sc-if>
4693                  <sc-if value="{{ rgIsInvernada }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO PROMEDIO<input type="number" value="" data-dc-attr-value="{{ rgAverageWeight }}" onInput="{{ onRgAverageWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÍNIMO<input type="number" value="" data-dc-attr-value="{{ rgMinWeight }}" onInput="{{ onRgMinWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÁXIMO<input type="number" value="" data-dc-attr-value="{{ rgMaxWeight }}" onInput="{{ onRgMaxWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESAJE<select value="" data-dc-attr-value="{{ rgWeighing }}" onChange="{{ onRgWeighing }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionar…</option><sc-for list="{{ rgWeighingOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">DESBASTE<input value="" data-dc-attr-value="{{ rgDesbaste }}" onInput="{{ onRgDesbaste }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label></div><div class="rg-state-toggles" style="margin-top:16px;margin-bottom:0;"><button type="button" class="rg-state-toggle" onClick="{{ toggleRgLiquid }}" aria-pressed="{{ rgLiquidPressed }}" style="color:{{ rgLiquidColor }};background:{{ rgLiquidBg }};border-color:{{ rgLiquidBorder }};box-shadow:{{ rgLiquidShadow }};"><img src="assets/icons/lucide/tag.svg" alt="" aria-hidden="true" width="21" height="21" style="width:21px;height:21px;filter:{{ rgLiquidIconFilter }};opacity:{{ rgLiquidIconOpacity }};"><span>Marca líquida</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgLiquidCheckBg }};color:{{ rgLiquidCheckColor }};">{{ rgLiquidCheck }}</span></button><button type="button" class="rg-state-toggle" onClick="{{ toggleRgVisec }}" aria-pressed="{{ rgVisecPressed }}" style="color:{{ rgVisecColor }};background:{{ rgVisecBg }};border-color:{{ rgVisecBorder }};box-shadow:{{ rgVisecShadow }};"><img src="assets/icons/visec-mark.svg" alt="" aria-hidden="true" width="23" height="23" style="width:23px;height:23px;filter:{{ rgVisecIconFilter }};opacity:{{ rgVisecIconOpacity }};"><span>Apto VISEC</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgVisecCheckBg }};color:{{ rgVisecCheckColor }};">{{ rgVisecCheck }}</span></button></div></sc-if>
4694                  <sc-if value="{{ rgIsVientres }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:12px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">SERVICIO<input type="text" value="" data-dc-attr-value="{{ rgService }}" onInput="{{ onRgService }}" placeholder="Ingresar servicio" style="height:44px;min-width:0;width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.34);background:rgba(255,255,255,.1);color:inherit;border-radius:11px;padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PREÑEZ<input type="text" value="" data-dc-attr-value="{{ rgPregnancy }}
4694" onInput="{{ onRgPregnancy }}" placeholder="Ingresar preñez" style="height:44px;min-width:0;width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.34);background:rgba(255,255,255,.1);color:inherit;border-radius:11px;padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PARICIÓN<input type="text" value="" data-dc-attr-value="{{ rgCalving }}" onInput="{{ onRgCalving }}" placeholder="Ingresar parición" style="height:44px;min-width:0;width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.34);background:rgba(255,255,255,.1);color:inherit;border-radius:11px;padding:0 12px;"></label></div></sc-if>
4695                  <sc-if value="{{ rgIsFaena }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">SUBCATEGORÍA<select value="" data-dc-attr-value="{{ rgSubCategory }}" onChange="{{ onRgSubCategory }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionar…</option><sc-for list="{{ rgSubCategoryOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO PROMEDIO<input type="number" value="" data-dc-attr-value="{{ rgAverageWeight }}" onInput="{{ onRgAverageWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÍNIMO<input type="number" value="" data-dc-attr-value="{{ rgMinWeight }}" onInput="{{ onRgMinWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÁXIMO<input type="number" value="" data-dc-attr-value="{{ rgMaxWeight }}" onInput="{{ onRgMaxWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">CONDICIÓN<select value="" data-dc-attr-value="{{ rgConditionType }}" onChange="{{ onRgConditionType }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionar…</option><sc-for list="{{ rgConditionTypeOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">ENTREGA<select value="" data-dc-attr-value="{{ rgDeliveryType }}" onChange="{{ onRgDeliveryType }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionar…</option><sc-for list="{{ rgDeliveryTypeOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">TERMINACIÓN<select value="" data-dc-attr-value="{{ rgFinishing }}" onChange="{{ onRgFinishing }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionar…</option><sc-for list="{{ rgFinishingOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">RENDIMIENTO ESTIMADO %<input type="number" step="0.1" value="" data-dc-attr-value="{{ rgEstimatedYield }}" onInput="{{ onRgEstimatedYield }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESAJE<select value="" data-dc-attr-value="{{ rgWeighing }}" onChange="{{ onRgWeighing }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionar…</option><sc-for list="{{ rgWeighingOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label></div></sc-if>
4696                </section>
4697              </sc-if>
4698
4699              <sc-if value="{{ rgIsStep3 }}" hint-placeholder-val="{{ false }}">
4700                <section>
4701                  <div style="display:flex; align-items:center; gap:12px; margin-bottom:19px;"><span style="width:34px; height:34px; border-radius:11px; background:#e9efff; color:#3158a8; display:grid; place-items:center; font-size:14px; font-weight:900;">4</span><div><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"><h2 style="margin:0; color:#102f50; font-size:18px;">Videos del lote</h2><span style="padding:3px 8px;border-radius:999px;background:#eef2f6;color:#66788b;font-size:10px;font-weight:850;letter-spacing:.06em;text-transform:uppercase;">Opcional</span></div><p style="margin:3px 0 0; color:#718296; font-size:12.5px;">Podés agregar videos ahora o finalizar y cargarlos más adelante.</p></div></div>
4702                  <p style="margin:0 0 16px;font-size:13px;"><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a></p>
4703                  <div style="display:flex; align-items:center; gap:10px; padding:12px 14px; border:1px solid #bfe2d5; border-radius:13px; background:#effaf6; color:#176c58;"><i class="dc-lucide dc-lucide-circle-check" aria-hidden="true" style="font-size:19px;"></i><div><strong style="display:block; font-size:13px;">Lote cargado para control</strong><span style="display:block; margin-top:2px; font-size:11.5px;">Certificado {{ rgCreatedLotCert }}</span></div></div>
4704                  <sc-if value="{{ rgCreatedLotCanUpload }}" hint-placeholder-val="{{ true }}">
4705                    <div style="margin-top:16px;"><button class="rg-filming-guide" type="button" onClick="{{ prepareCreatedFieldLot }}" style="margin:0;"><img src="/assets/icons/lucide/camera.svg" alt="" aria-hidden="true" style="width:18px;height:18px;">Modo filmación sin internet</button><p style="max-width:600px;margin:8px 0 0;color:#60758c;font-size:12px;line-height:1.5;">Preparalo con conexión antes de salir. Después podés filmar sin señal: los videos se guardan en este dispositivo y se suben cuando vuelva internet.</p></div>
4706                    <label style="display:grid; place-items:center; min-height:190px; margin-top:16px; border:2px dashed #9fc2df; border-radius:17px; background:linear-gradient(145deg,#f8fbfe,#eef6fc); color:#0a5ca8; text-align:center; cursor:pointer; padding:22px; box-sizing:border-box;"><input type="file" multiple accept="video/*,.mp4,.mov,.m4v,.webm,.avi,.mpeg,.mpg" disabled="{{ rgCreatedVideoBusy }}" onChange="{{ uploadRgCreatedLotVideos }}" style="display:none;"><span><i class="dc-lucide dc-lucide-upload-cloud" aria-hidden="true" style="font-size:34px;"></i><strong style="display:block; margin-top:10px; font-size:15px;
4706 color:#10375d;">Seleccionar videos</strong><small style="display:block; margin-top:5px; color:#6e8296; line-height:1.45;">Podés elegir varios archivos a la vez.<br>Hasta 10 videos por vez · Máximo 1 GB cada uno.</small></span></label>
4707                  </sc-if>
4708                  <sc-if value="{{ rgCreatedLotCannotUpload }}" hint-placeholder-val="{{ false }}"><div style="margin-top:15px; padding:13px 14px; border:1px solid #efc7cb; border-radius:12px; background:#fff4f5; color:#a32934; font-size:12.5px;">El lote fue creado, pero el API no devolvió su certificado. Podés finalizar y cargar los videos luego desde el remate.</div></sc-if>
4709                  <sc-if value="{{ rgCreatedVideoHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; padding:10px 12px; border-radius:10px; background:#fff3f4; border:1px solid #efc6ca; color:#a32934; font-size:12.5px;">{{ rgCreatedVideoError }}</div></sc-if>
4710                  <sc-if value="{{ rgCreatedVideoLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; justify-content:center; gap:9px; margin-top:14px; padding:13px; border-radius:11px; background:#f6f9fc; color:#536b83; font-size:12px;"><i class="dc-lucide dc-lucide-loader-circle rg-spin" aria-hidden="true"></i>Cargando videos del lote…</div></sc-if>
4711                  <sc-if value="{{ rgCreatedVideoHasUploads }}" hint-placeholder-val="{{ false }}"><div style="display:grid; gap:9px; margin-top:14px;"><sc-for list="{{ rgCreatedVideoUploads }}" as="u" hint-placeholder-count="2"><div style="border:1px solid #dce7f2; border-radius:12px; padding:11px 13px; background:#f8fbfe;"><div style="display:flex; justify-content:space-between; gap:12px; font-size:12px;"><strong style="color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.name }}</strong><span style="color:{{ u.color }}; flex:none;">{{ u.status }}</span></div><div style="height:6px; border-radius:10px; overflow:hidden; background:#e1eaf3; margin-top:8px;"><span style="display:block; width:{{ u.progress }}%; height:100%; border-radius:inherit; background:{{ u.bar }}; transition:width .2s ease;"></span></div><div style="font-size:10.5px; color:#7b8ca1; margin-top:5px;">{{ u.size }}</div><sc-if value="{{ u.hasError }}"><div style="font-size:11.5px;color:#b42332;margin-top:7px;">{{ u.error }}</div></sc-if><sc-if value="{{ u.canRetry }}"><button type="button" onClick="{{ u.retry }}" style="margin-top:8px;border:1px solid #bbd1e8;border-radius:9px;padding:8px 11px;background:#fff;color:#0a5ca8;font-weight:700;">{{ u.retryLabel }}</button></sc-if></div></sc-for></div></sc-if>
4712                  <sc-if value="{{ rgCreatedVideosHasItems }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; margin-top:14px;"><sc-for list="{{ rgCreatedVideos }}" as="v" hint-placeholder-count="2"><article style="border:1px solid #dce7f2; border-radius:13px; overflow:hidden; background:#fff;"><sc-if value="{{ v.hasUrl }}" hint-placeholder-val="true"><video controls preload="metadata" data-dc-attr-src="{{ v.url }}" style="width:100%; aspect-ratio:16/9; display:block; background:#061b32;"></video></sc-if><sc-if value="{{ v.noUrl }}" hint-placeholder-val="false"><div style="aspect-ratio:16/9; display:grid; place-items:center; background:#eef4fa; color:#0a5ca8;"><i class="dc-lucide dc-lucide-file-video" aria-hidden="true" style="font-size:27px;"></i></div></sc-if><div style="display:flex; align-items:center; gap:8px; padding:9px 10px;"><div style="min-width:0; flex:1;"><strong style="display:block; font-size:11.5px; color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.name }}</strong><span style="font-size:10px; color:#7b8ca1;">{{ v.size }}</span></div><button type="button" onClick="{{ v.remove }}" disabled="{{ v.deleting }}" title="Eliminar video" style="width:31px; height:31px; border-radius:9px; border:1px solid #f0c8cc; background:#fff5f5; color:#ba2d3a; display:grid; place-items:center; cursor:pointer;"><i class="dc-lucide dc-lucide-trash-2" aria-hidden="true"></i></button></div></article></sc-for></div></sc-if>
4713                  <sc-if value="{{ rgCreatedVideosEmpty }}" hint-placeholder-val="{{ false }}"><div style="margin-top:14px; padding:12px; border-radius:11px; background:#f6f9fc; color:#536b83; font-size:12px; text-align:center;"><strong style="color:#26384d;">El lote quedó en estado Cargado.</strong> Los videos son opcionales: podés finalizar ahora y agregarlos más tarde durante la carga.</div></sc-if>
4714                </section>
4715              </sc-if>
4716
4717                </div>
4718                <div class="rg-wizard-footer" style="display:flex; justify-content:space-between; gap:12px; margin-top:22px; padding-top:18px; border-top:1px solid #eef3f9;"><button type="button" onClick="{{ rgPrevStep }}" style="visibility:{{ rgPrevVisible }}; display:inline-flex;align-items:center;gap:6px;background:#eef3f9; color:#002e5f; border:1px solid #dde7f2; border-radius:12px; padding:12px 22px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Atrás</button><button type="button" onClick="{{ rgNextStep }}" disabled="{{ rgNextDisabled }}" style="display:inline-flex;align-items:center;justify-content:center;gap:7px;background:{{ rgNextBg }}; color:#fff; border:none; border-radius:12px; padding:12px 26px; min-width:150px; font-size:14px; font-weight:800; font-family:inherit; cursor:pointer; box-shadow:0 8px 20px rgba(10,92,168,.18); opacity:{{ rosganLotPublishOpacity }};">{{ rgNextLabel }}<sc-if value="{{ rgNextShowsArrow }}" hint-placeholder-val="{{ true }}"><i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></sc-if></button></div>
4719              </div>
4720              <sc-if value="{{ rgShowPreview }}" hint-placeholder-val="{{ true }}">
4721                <aside class="rg-preview-panel" style="position:sticky; top:82px;">
4722                  <div style="font-size:11.5px; letter-spacing:.12em; color:#8593a6; font-weight:700; margin-bottom:10px;">PREVISUALIZACIÓN</div>
4723                  <div style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 18px 50px rgba(0,30,60,.14);">
4724                    <div style="position:relative; min-height:180px; padding:22px 20px; background:{{ rgPreviewBrandColor }}; color:#fff; display:flex; flex-direction:column; justify-content:flex-end; overflow:hidden;">
4725                      <sc-if value="{{ rgPreviewHasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ rgPreviewLogo }}" alt="{{ rgPreviewConsignataria }}" style="position:absolute;
4725left:50%;top:45px;transform:translateX(-50%);width:116px;height:88px;object-fit:contain;"></sc-if>
4726                      <sc-if value="{{ rgPreviewHasThumbnail }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ rgPreviewThumbnail }}" alt="" onError="{{ rgPreviewImageError }}" onLoad="{{ rgPreviewImageLoaded }}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></sc-if>
4727                      <div aria-hidden="true" style="position:absolute;inset:0;background:linear-gradient(180deg,transparent 45%,rgba(0,20,40,.65));pointer-events:none;"></div>
4728                      <span style="position:absolute; top:13px; left:14px; border:1px solid rgba(255,255,255,.25); background:rgba(0,20,40,.22); border-radius:100px; padding:5px 10px; font-size:10px; font-weight:800; letter-spacing:.08em;">{{ rgPreviewType }}</span>
4729                      <div style="position:relative;font-size:19px; font-weight:850; line-height:1.08;">{{ rgPreviewCategory }}</div><div style="position:relative;font-size:11.5px; color:#e5eef7; margin-top:5px;">{{ rgPreviewRemate }}</div>
4730                    </div>
4731                    <div style="padding:15px 16px; display:grid; gap:12px;">
4732                      <div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;"><div style="background:#f2f6fb; border-radius:10px; padding:10px 12px;"><div style="font-size:10px; color:#8593a6;">CABEZAS</div><div style="font-size:17px; font-weight:800; color:#112033;">{{ rgPreviewHeads }}</div></div><div style="background:#f2f6fb; border-radius:10px; padding:10px 12px;"><div style="font-size:10px; color:#8593a6;">PESO PROM.</div><div style="font-size:17px; font-weight:800; color:#112033;">{{ rgPreviewWeight }}</div></div></div>
4733                      <div style="display:grid; gap:7px; font-size:12px;"><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Consignataria</span><strong style="color:#112033; text-align:right;">{{ rgPreviewConsignataria }}</strong></div><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Comitente</span><strong style="color:#112033; text-align:right;">{{ rgPreviewComitente }}</strong></div><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Establecimiento</span><strong style="color:#112033; text-align:right;">{{ rgPreviewEstablishment }}</strong></div><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Raza</span><strong style="color:#112033; text-align:right;">{{ rgPreviewBreeds }}</strong></div></div>
4734                      <div style="display:flex;justify-content:space-between;gap:10px;font-size:12px;"><span style="color:#8593a6;">Plazo de pago</span><strong style="text-align:right;color:#112033;">{{ rgPreviewPaymentTerm }}</strong></div><div><div style="display:flex; justify-content:space-between; margin-bottom:6px; font-size:11px; font-weight:700;"><span style="color:#8593a6;">Completitud</span><span style="color:#0a5ca8;">{{ rgPreviewCompleteness }}</span></div><div style="height:6px; background:#e7eef6; border-radius:100px; overflow:hidden;"><div style="height:100%; width:{{ rgPreviewProgress }}; border-radius:100px; background:linear-gradient(90deg,#0a5ca8,#24b4bd); transition:width 240ms;"></div></div></div>
4735                    </div>
4736                  </div>
4737                </aside>
4738              </sc-if>
4739            </div>
4740          </sc-if>
4741        </div>
4742      </div>
4743    </sc-if>
4744
4745    <!-- ============ CARGAR LOTE (WIZARD) ============ -->
4746    <sc-if value="{{ isLoad }}" hint-placeholder-val="{{ false }}">
4747      <div class="lot-wizard-page" style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;">
4748        <div class="lot-wizard-header" style="display:flex; justify-content:space-between; align-items:flex-end; gap:14px; flex-wrap:wrap; padding:24px 0 20px;">
4749          <div>
4750            <span onClick="{{ wizardBack }}" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:#0a5ca8; font-weight:600; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>{{ wizardBackLabel }}</span>
4751            <h1 style="font-size:clamp(23px, 3.2vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:8px 0 2px; color:#112033;">{{ wizardTitle }}</h1>
4752            <span style="font-size:13.5px; color:#65758a;">{{ wizardSubtitle }}</span>
4753          </div>
4754          <span onClick="{{ resetForm }}" style="font-size:13px; color:#e63946; font-weight:600; cursor:pointer;">{{ wizardResetLabel }}</span>
4755        </div>
4756        <!-- step progress -->
4757        <div class="lot-wizard-steps" style="display:flex; gap:8px; margin-bottom:20px;">
4758          <sc-for list="{{ wizardSteps }}" as="w" hint-placeholder-count="4">
4759            <div class="rg-wizard-step" onClick="{{ w.go }}" style="flex:1; min-width:0; cursor:pointer;">
4760              <div style="height:5px; border-radius:100px; background:{{ w.barBg }}; transition:background 300ms;"></div>
4761              <div style="display:flex; align-items:center; gap:8px; margin-top:9px;">
4762               <span style="width:24px; height:24px; border-radius:50%; display:grid; place-items:center; font-size:12px; font-weight:800; flex-shrink:0; background:{{ w.dotBg }}; color:{{ w.dotColor }}; border:1px solid {{ w.dotBorder }}; transition:all 260ms;"><sc-if value="{{ w.done }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if><sc-if value="{{ w.numberVisible }}" hint-placeholder-val="{{ true }}">{{ w.number }}</sc-if></span>
4763                <div class="lot-wizard-step-copy" style="min-width:0;"><div style="font-size:13px; font-weight:700; color:{{ w.titleColor }}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.title }}</div><div style="font-size:11px; color:#8593a6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.text }}</div></div>
4764              </div>
4765            </div>
4766          </sc-for>
4767        </div>
4768        <div class="lot-wizard-grid" style="display:grid; grid-template-columns:{{ loadLayoutCols }}; gap:20px; align-items:start;">
4769          <!-- form panel -->
4770          <div class="lot-form-panel" style="background:#fff; border-radius:20px; box-shadow:0 12px 40px rgba(0,30,60,0.1); padding:clamp(20px, 3vw, 30px); min-height:420px; display:flex; flex-direction:column;">
4771            <div style="animation:pageIn 340ms cubic-bezier(0.22,0.72,0.18,1) both; flex:1;" key="{{ stepKey }}">
4772
4773              <!-- STEP 0 -->
4774              <sc-if value="{{ isStep0 }}" hint-placeholder-val="{{ true }}">
4775                <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">ETAPA 1</div>
4776                <h2 style="font-size:20px; font-weight:800; margin:4px 0 18px; color:#112033;">Establecimiento, categoría y cantidad</h2>
4777                <div style="display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap; margin-bottom:16px;">
4778                  <sc-if value="{{ estSelectorHas }}" hint-placeholder-val="{{ false }}">
4779                    <div style="flex:1; min-width:220px;">
4780                      <div style="font-size:12px; font-weight:700; color:#3d4c5f; margin-bottom:6px;">Usar un establecimiento guardado</div>
4781                      <select value="" data-dc-attr-value="{{ selectedEstId }}" onChange="{{ pickEst }}" style="width:100%; border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;" style-focus="border-color:#0a5ca8;">
4782                        <option value="">Seleccioná un establecimiento</option>
4783                        <sc-for list="{{ estSelectOptions }}" as="o" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for>
4784                      </select>
4785                    </div>
4786                  </sc-if>
4787                  <sc-if value="{{ canCreateEstablishment }}" hint-placeholder-val="{{ false }}"><button onClick="{{ wzEstToggle }}" style="background:#0a5ca8; color:#fff; border:none; border-radius:10px; padding:11px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap;" style-hover="background:#084b8a;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nuevo establecimiento</button></sc-if>
4788                  <sc-if value="{{ estIsSaved }}" hint-placeholder-val="{{ false }}">
4789                    <span style="display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:700; color:#1f7a63; background:rgba(31,122,99,0.12); padding:9px 13px; border-radius:100px;"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i>Guardado</span>
4790                  </sc-if>
4791                </div>
4792                <sc-if value="{{ wzEstOpen }}" hint-placeholder-val="{{ false }}">
4793                  <div class="establishment-form-card" style="background:#f6f9fc; border:1px solid #e3eaf2; border-radius:14px; padding:18px 20px; margin-bottom:18px;">
4794                    <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px;"><div><div style="font-size:14.5px; font-weight:800; color:#112033;">Nuevo establecimiento</div><div style="margin-top:3px; font-size:11.5px; color:#65758a;">Primero elegí o creá el comitente al que pertenece.</div></div><button type="button" onClick="{{ wzToggleComitente }}" style="display:inline-flex; align-items:center; gap:7px; border:1px solid #b9d2e8; border-radius:9px; background:#fff; color:#0a5ca8; padding:8px 11px; font:800 11px inherit; cursor:pointer; white-space:nowrap;"><img src="assets/icons/lucide/users-round.svg" alt="" style="width:15px; height:15px;"> Crear comitente</button></div>
4795                    <sc-if value="{{ wzComitenteOpen }}" hint-placeholder-val="{{ false }}">
4796                      <div style="margin-bottom:13px; padding:13px; border:1px solid #bdd8eb; border-radius:12px; background:#fff;">
4797                        <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px;"><div><strong style="display:block; color:#123b61; font-size:13px;">Alta de comitente</strong><span style="display:block; margin-top:2px; color:#718296; font-size:10.5px;">Ingresá el CUIT: razón social y domicilio fiscal se completan desde ARCA.</span></div><button type="button" onClick="{{ wzToggleComitente }}" aria-label="Cerrar" style="width:28px;height:28px;border:0;border-radius:8px;background:#eef4f9;color:#52677c;cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="" style="width:14px;height:14px;"></button></div>
4798                        <div class="establishment-form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
4799                          <label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">CUIT (opcional)</span><span style="position:relative;"><input aria-describedby="confidential-wz_cf_cuit" value="" data-dc-attr-value="{{ wz_cf_cuit }}" onInput="{{ wz_set_cf_cuit }}" inputmode="numeric" maxlength="11" placeholder="11 dígitos" style="box-sizing:border-box;width:100%;border:1px solid #d5e2ed;border-radius:9px;padding:10px 36px 10px 11px;font:650 13px inherit;color:#112033;"><sc-if value="{{ wzComitenteLookup }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:11px;top:11px;color:#0a5ca8;"></i></sc-if></span><small id="confidential-wz_cf_cuit" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">&#9888;</span><span>Dato confidencial perteneciente a la firma consignataria</span></small></label>
4800                          <label style="display:grid;gap:6px;color:#60758c;font-size:12px;font-weight:700;">Alias (opcional)<input type="text" value="" data-dc-attr-value="{{ wz_cf_alias }}" onInput="{{ wz_set_cf_alias }}" placeholder="Ej.: Estancia El Ombú" style="box-sizing:border-box;min-width:0;width:100%;height:43px;border:1px solid #ccdae8;border-radius:11px;padding:0 12px;font:inherit;background:#fff;"></label>
4801<label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">RAZÓN SOCIAL</span><input value="" data-dc-attr-value="{{ wz_cf_razon }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #d5e2ed;border-radius:9px;padding:10px 11px;font:650 13px inherit;color:#112033;background:#eef3f7;"></label>
4802                          <label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">PROVINCIA FISCAL</span><input value="" data-dc-attr-value="{{ wz_cf_provincia }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #d5e2ed;border-radius:9px;padding:10px 11px;font:650 13px inherit;color:#112033;background:#eef3f7;"></label>
4803                          <label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">LOCALIDAD FISCAL</span><input value="" data-dc-attr-value="{{ wz_cf_localidad }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #d5e2ed;border-radius:9px;padding:10px 11px;font:650 13px inherit;color:#112033;background:#eef3f7;"></label>
4804                        </div>
4805                        <sc-if value="{{ wzComitenteMsg }}" hint-placeholder-val="{{ false }}"><div style="margin-top:9px;padding:8px 10px;border-radius:8px;background:{{ wzComitenteMsgBg }};color:{{ wzComitenteMsgColor }};font-size:11px;font-weight:700;">{{ wzComitenteMsg }}</div></sc-if>
4806                        <div style="display:flex;justify-content:flex-end;margin-top:10px;"><button type="button" onClick="{{ wzSaveComitente }}" disabled="{{ wzComitenteSaveDisabled }}" style="border:0;border-radius:9px;background:#1f7a63;color:#fff;padding:10px 15px;font:800 11.5px inherit;cursor:pointer;opacity:{{ wzComitenteSaveOpacity }};">{{ wzComitenteSaveLabel }}</button></div>
4807                      </div>
4808                    </sc-if>
4809                    <sc-if value="{{ wzNoComitentes }}" hint-placeholder-val="{{ false }}"><div style="margin-bottom:12px; padding:10px 12px; border:1px solid #f0d4a6; border-radius:10px; background:#fff8e8; color:#805617; font-size:12px; font-weight:700;">Todavía no tenés comitentes. Podés crear el primero acá mismo.</div></sc-if>
4810                    <div class="establishment-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
4811                      <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Comitente *</span><select value="" data-dc-attr-value="{{ wz_ef_comitenteId }}" onChange="{{ wz_set_ef_comitenteId }}" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none;
4811 color:#112033; background:#fff;"><option value="">Seleccionar comitente…</option><sc-for list="{{ wzEfComitenteOpts }}" as="c" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ c.id }}">{{ c.label }}</option></sc-for></select></label>
4812                      <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Nombre del establecimiento *</span><input value="" data-dc-attr-value="{{ wz_ef_nombre }}" onInput="{{ wz_set_ef_nombre }}" placeholder="Ej: Estancia El Ceibo" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
4813                      <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Provincia *</span><select value="" data-dc-attr-value="{{ wz_ef_provincia }}" onChange="{{ wz_set_ef_provincia }}" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">{{ wzEfProvPlaceholder }}</option><sc-for list="{{ wzEfProvinceOpts }}" as="p" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for></select></label>
4814                      <label style="position:relative;display:flex; flex-direction:column; gap:6px;"><span style="display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px; font-weight:600; color:#3d4c5f;"><span>Localidad *</span><small style="display:inline-flex;align-items:center;gap:4px;color:#718296;font-weight:600;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;object-fit:contain;">Google</small></span><input value="" data-dc-attr-value="{{ wz_ef_localidad }}" onInput="{{ wz_set_ef_localidad }}" onFocus="{{ wzOpenPlaceResults }}" onBlur="{{ wzClosePlaceResults }}" disabled="{{ wzEfLocDisabled }}" autocomplete="off" placeholder="Escribí una localidad" style="border:1px solid #dde7f2;border-radius:10px;padding:10px 36px 10px 12px;font-size:14px;font-family:inherit;outline:none;color:#112033;background:{{ wzEfLocBg }};"><sc-if value="{{ wzPlaceLoading }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:12px;top:35px;color:#0a5ca8;"></i></sc-if><sc-if value="{{ wzPlaceResultsOpen }}" hint-placeholder-val="{{ false }}"><div style="position:absolute;z-index:50;left:0;right:0;top:calc(100% + 5px);padding:5px;border:1px solid #d6e2ed;border-radius:11px;background:#fff;box-shadow:0 14px 30px rgba(0,35,70,.18);max-height:220px;overflow:auto;"><sc-for list="{{ wzPlaceResults }}" as="p" hint-placeholder-count="4"><button type="button" onClick="{{ p.pick }}" style="width:100%;display:grid;gap:2px;border:0;border-radius:8px;background:#fff;padding:8px 9px;text-align:left;cursor:pointer;"><strong style="font-size:11.5px;color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px;color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if></label>
4815                      <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">RENSPA <small style="font-weight:500;color:#8593a6;">opcional</small></span><input value="" data-dc-attr-value="{{ wz_ef_renspa }}" onInput="{{ wz_set_ef_renspa }}" placeholder="Podés dejarlo vacío" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label>
4816                    </div>
4817                    <sc-if value="{{ wzEstFormMsg }}" hint-placeholder-val="{{ false }}">
4818                      <div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ wzEstFormMsgBg }}; color:{{ wzEstFormMsgColor }};">{{ wzEstFormMsg }}</div>
4819                    </sc-if>
4820                    <sc-if value="{{ wzPlaceError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:9px;font-size:10.5px;color:#8a5a00;">{{ wzPlaceError }}</div></sc-if>
4821                    <div style="display:flex; gap:10px; margin-top:14px;">
4822                      <button onClick="{{ wzSaveEst }}" disabled="{{ wzEstSaveDisabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:{{ wzEstSaveCursor }}; opacity:{{ wzEstSaveOpacity }};">{{ wzEstSaveLabel }}</button>
4823                      <button onClick="{{ wzEstToggle }}" style="background:#fff; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:10px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button>
4824                    </div>
4825                  </div>
4826                </sc-if>
4827                <div class="lot-fields-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
4828                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Nombre del establecimiento</span><input value="" data-dc-attr-value="{{ f_estName }}" disabled="{{ manualEstLocked }}" placeholder="Seleccioná un establecimiento validado" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
4828 color:#112033; background:#eef3f8;"></label>
4829                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Comitente</span><input value="" data-dc-attr-value="{{ f_comitente }}" disabled="{{ manualEstLocked }}" placeholder="Se obtiene del establecimiento" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f8;"></label>
4830                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Provincia</span><input value="" data-dc-attr-value="{{ f_prov }}" disabled="{{ manualEstLocked }}" placeholder="Se obtiene del establecimiento" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f8;"></label>
4831                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Localidad</span><input value="" data-dc-attr-value="{{ f_loc }}" disabled="{{ manualEstLocked }}" placeholder="Se obtiene del establecimiento" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f8;"></label>
4832                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha de subasta</span><input value="" data-dc-attr-value="{{ f_fecha }}" onInput="{{ set_fecha }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
4833                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Categoría</span><select value="" data-dc-attr-value="{{ f_cat }}" onChange="{{ set_cat }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">Seleccioná</option><sc-for list="{{ categoryOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for></select></label>
4834                  <sc-if value="{{ subcatHas }}" hint-placeholder-val="{{ false }}">
4835                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Subcategoría</span><select value="" data-dc-attr-value="{{ f_subcat }}" onChange="{{ set_subcat }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;" style-focus="border-color:#0a5ca8;"><option value="">Seleccioná</option><sc-for list="{{ subcatOptions }}" as="o" hint-placeholder-count="3"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for></select></label>
4836                  </sc-if>
4837                  <sc-if value="{{ catNotMixed }}" hint-placeholder-val="{{ true }}">
4838                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">{{ cantLabel }}</span><input value="" data-dc-attr-value="{{ f_cant }}" onInput="{{ set_cant }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
4839                  </sc-if>
4840                  <sc-if value="{{ catIsMixed }}" hint-placeholder-val="{{ false }}">
4841                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Cantidad machos</span><input value="" data-dc-attr-value="{{ f_cantMachos }}" onInput="{{ set_cantMachos }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
4841 color:#112033;" style-focus="border-color:#0a5ca8;"></label>
4842                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Cantidad hembras</span><input value="" data-dc-attr-value="{{ f_cantHembras }}" onInput="{{ set_cantHembras }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
4843                  </sc-if>
4844                </div>
4845                <div style="margin-top:18px;">
4846                  <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Razas <span style="color:#8593a6; font-weight:500;">(máximo 3 · {{ razasCount }}/3)</span></div>
4847                  <div style="display:flex; gap:8px; flex-wrap:wrap;">
4848                    <sc-for list="{{ breedOptions }}" as="b" hint-placeholder-count="6">
4849                      <span onClick="{{ b.toggle }}" style="padding:7px 14px; border-radius:100px; font-size:12.5px; font-weight:600; cursor:pointer; border:1px solid {{ b.border }}; background:{{ b.bg }}; color:{{ b.color }}; transition:all 150ms;" style-hover="border-color:#0a5ca8;">{{ b.label }}</span>
4850                    </sc-for>
4851                  </div>
4852                </div>
4853              </sc-if>
4854
4855              <!-- STEP 1 -->
4856              <sc-if value="{{ isStep1 }}" hint-placeholder-val="{{ false }}">
4857                <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">ETAPA 2</div>
4858                <h2 style="font-size:20px; font-weight:800; margin:4px 0 18px; color:#112033;">Engorde, destino y pesos</h2>
4859                <div style="display:flex; flex-direction:column; gap:18px;">
4860                  <div>
4861                    <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Tipo de engorde</div>
4862                    <div style="display:flex; gap:8px; flex-wrap:wrap;">
4863                      <sc-for list="{{ fatteningOptions }}" as="o" hint-placeholder-count="3"><span onClick="{{ o.pick }}" style="padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }}; transition:all 150ms;">{{ o.label }}</span></sc-for>
4864                    </div>
4865                    <sc-if value="{{ showDiasEncierre }}" hint-placeholder-val="{{ false }}">
4866                      <label style="display:flex; flex-direction:column; gap:6px; margin-top:12px; max-width:220px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Días de encierre</span><input value="" data-dc-attr-value="{{ f_diasEncierre }}" onInput="{{ set_diasEncierre }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4867                    </sc-if>
4868                  </div>
4869                  <div>
4870                    <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Destino</div>
4871                    <div style="display:flex; gap:8px; flex-wrap:wrap;">
4872                      <sc-for list="{{ destinationOptions }}" as="o" hint-placeholder-count="4"><span onClick="{{ o.pick }}" style="padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }}; transition:all 150ms;">{{ o.label }}</span></sc-for>
4873                    </div>
4874                    <sc-if value="{{ hiltonRestricted }}" hint-placeholder-val="{{ false }}"><div style="margin-top:8px; color:#8a5a00; font-size:11.5px; line-height:1.4;"><i class="dc-lucide dc-lucide-info" aria-hidden="true"></i> Ración o Feedlot no pueden tener destino Exportación Hilton.</div></sc-if>
4875                  </div>
4876                  <div>
4877                    <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">¿Apto China?</div>
4878                    <div style="display:flex; gap:8px;">
4879                      <sc-for list="{{ chinaOptions }}" as="o" hint-placeholder-count="2"><span onClick="{{ o.pick }}" style="padding:9px 20px; border-radius:100px; font-size:13px; font-weight:700; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for>
4880                    </div>
4881                  </div>
4882                  <div class="lot-three-fields-grid" style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px;">
4883                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Peso promedio (kg)</span><input value="" data-dc-attr-value="{{ f_pesoProm }}" onInput="{{ set_pesoProm }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4884                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Peso mínimo (kg)</span><input value="" data-dc-attr-value="{{ f_pesoMin }}" onInput="{{ set_pesoMin }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4885                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Peso máximo (kg)</span><input value="" data-dc-attr-value="{{ f_pesoMax }}" onInput="{{ set_pesoMax }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4886                  </div>
4887                </div>
4888              </sc-if>
4889
4890              <!-- STEP 2 -->
4891              <sc-if value="{{ isStep2 }}" hint-placeholder-val="{{ false }}">
4892                <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">ETAPA 3</div>
4893                <h2 style="font-size:20px; font-weight:800; margin:4px 0 18px; color:#112033;">Condición comercial y disponibilidad</h2>
4894                <div style="display:flex; flex-direction:column; gap:18px;">
4895                  <div>
4896                    <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Condición de venta</div>
4897                    <div style="display:flex; gap:8px; flex-wrap:wrap;">
4898                      <sc-for list="{{ conditionOptions }}" as="o" hint-placeholder-count="2"><span onClick="{{ o.pick }}" style="padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for>
4899                    </div>
4900                  </div>
4901                  <div class="lot-fields-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
4902                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Precio sugerido</span><input value="" data-dc-attr-value="{{ f_precio }}" onInput="{{ set_precio }}" type="text" inputmode="numeric" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4903                    <div style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Disponibilidad de carga</span><button type="button" onClick="{{ toggleListoParaCargar }}" aria-pressed="{{ f_listoParaCargar }}" style="min-height:43px; border:1px solid {{ f_listoBorder }}; border-radius:10px; padding:9px 13px; background:{{ f_listoBg }}; color:{{ f_listoColor }}; text-align:left; font:700 13px inherit; cursor:pointer;">{{ f_listoLabel }}</button><span style="font-size:11px; color:#65758a;">Si está listo, la carga queda disponible desde la fecha de subasta.</span></div>
4904                    <sc-if value="{{ f_showFechaCarga }}" hint-placeholder-val="true"><label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha disponible de carga *</span><input value="" data-dc-attr-value="{{ f_dispo }}" data-dc-attr-min="{{ f_dispoMin }}" data-dc-attr-max="{{ f_dispoMax }}" onInput="{{ set_dispo }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"><span style="font-size:11px; color:#65758a;">Entre 1 y 7 días corridos después de la subasta.</span></label></sc-if>
4905                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Desbaste (%)</span><input value="" data-dc-attr-value="{{ f_desbaste }}" onInput="{{ set_desbaste }}" type="number" min="0" max="100" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4906                    <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Rendimiento estimado (%)</span><input value="" data-dc-attr-value="{{ f_rinde }}" onInput="{{ set_rinde }}" type="number" min="1" max="100" step="0.1" placeholder="Ej. 56" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label>
4907                  </div>
4908                  <div class="lot-fields-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
4909                    <div><div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Pesaje</div><div style="display:flex; gap:8px; flex-wrap:wrap;"><sc-for list="{{ weighingOptions }}" as="o" hint-placeholder-count="3"><span onClick="{{ o.pick }}" style="padding:8px 14px; border-radius:100px; font-size:12.5px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for></div></div>
4910                    <div><div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Plazo de pago</div><div style="display:flex; gap:8px; flex-wrap:wrap;"><sc-for list="{{ termOptions }}" as="o" hint-placeholder-count="4"><span onClick="{{ o.pick }}" style="padding:8px 14px; border-radius:100px; font-size:12.5px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for></div></div>
4911                  </div>
4912                  <sc-if value="{{ showPlazoOtro }}" hint-placeholder-val="{{ false }}">
4913                    <label style="display:flex; flex-direction:column; gap:6px; max-width:320px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Detalle del plazo</span><input value="" data-dc-attr-value="{{ f_plazoOtro }}" onInput="{{ set_plazoOtro }}" placeholder="Especificá el plazo acordado" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
4914                  </sc-if>
4915                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Observaciones</span><textarea value="" data-dc-attr-value="{{ f_obs }}" onInput="{{ set_obs }}" rows="3" placeholder="Estado sanitario, características, certificados…" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; resize:vertical; color:#112033;" style-focus="border-color:#0a5ca8;"></textarea></label>
4916                </div>
4917              </sc-if>
4918
4919              <!-- STEP 3: VIDEOS -->
4920              <sc-if value="{{ isStep3 }}" hint-placeholder-val="{{ false }}">
4921                <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">FINAL</div>
4922                <h2 style="font-size:20px; font-weight:800; margin:4px 0 6px; color:#112033;">Videos del lote</h2>
4923                <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:12px; flex-wrap:wrap; margin:4px 0 14px;">
4924                  <p style="font-size:13px; color:#65758a; margin:0; max-width:420px;">Subí hasta 3 videos 16:9. Elegí una portada por video y marcá cuál representa al lote.</p>
4925                  <sc-if value="{{ videosAnyPending }}" hint-placeholder-val="{{ true }}">
4926                    <button onClick="{{ uploadAllVideos }}" style="display:inline-flex; align-items:center; gap:7px; background:#0a5ca8; color:#fff; border:none; border-radius:10px; padding:9px 16px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap;" style-hover="background:#084b8a;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Seleccionar varios videos</button><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a><button class="rf-camera-entry" type="button" onClick="{{ filmAssistedVideo }}">◉ Filmar con asistente</button>
4927                  </sc-if>
4928                </div>
4929                <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:12px;">
4930                  <sc-for list="{{ videoSlots }}" as="v" hint-placeholder-count="3">
4931                    <div style="border:1px solid #e6edf5; border-radius:14px; background:#fff; overflow:hidden; box-shadow:0 4px 14px rgba(0,30,60,0.05);">
4932                      <!-- 16:9 stage -->
4933                      <div style="position:relative; aspect-ratio:16/9; background:#001427; overflow:hidden;">
4934                        <sc-if value="{{ v.empty }}" hint-placeholder-val="{{ true }}">
4935                          <div onClick="{{ v.upload }}" style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; cursor:pointer; border:2px dashed rgba(255,255,255,0.16); border-radius:10px; margin:8px; background:radial-gradient(120% 120% at 50% 0%, rgba(10,92,168,0.28), transparent 70%);" style-hover="border-color:rgba(90,170,255,0.6);">
4936                      <span style="width:34px; height:34px; border-radius:50%; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); display:grid; place-items:center; color:#bcd8f5; font-size:16px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span>
4937                            <span style="color:#e6f0fb; font-size:12px; font-weight:700;">Video {{ v.n }}</span>
4938                          </div>
4939                        </sc-if>
4940                        <sc-if value="{{ v.uploading }}" hint-placeholder-val="{{ false }}">
4941                          <div style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:9px; padding:0 16px;">
4942                            <span style="width:15px; height:15px; border:2px solid rgba(255,255,255,0.25); border-top-color:#5aa8ff; border-radius:50%; animation:spin 0.7s linear infinite; display:inline-block;"></span>
4943                            <div style="width:100%; height:6px; border-radius:100px; background:rgba(255,255,255,0.12); overflow:hidden; position:relative;">
4944                              <div style="height:100%; width:{{ v.progressPct }}; border-radius:100px; background:linear-gradient(90deg,#0a5ca8,#5aa8ff); transition:width 180ms ease;"></div>
4945                              <div style="position:absolute; top:0; bottom:0; width:40%; background:linear-gradient(90deg, transparent, rgba(255,255,255,0.45), transparent); animation:loadSlide 1.1s ease-in-out infinite;"></div>
4946                            </div>
4947                            <div style="font-size:15px; font-weight:800; color:#fff; font-variant-numeric:tabular-nums;">{{ v.progressPct }}</div>
4948                          </div>
4949                        </sc-if>
4950                        <sc-if value="{{ v.filled }}" hint-placeholder-val="{{ false }}">
4951                          <div style="position:absolute; inset:0; animation:fadeScale 320ms ease both;">
4952                            <div style="position:absolute; inset:0; background-image:url('{{ v.poster }}'); background-size:cover; background-position:center;"></div>
4953                            <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,39,0.1) 40%, rgba(0,20,39,0.62));"></div>
4954                          <span onClick="{{ v.remove }}" style="position:absolute; top:6px; right:6px; width:22px; height:22px; border-radius:50%; background:rgba(0,20,39,0.6); color:#fff; display:grid; place-items:center; font-size:13px; cursor:pointer;" style-hover="background:#e63946;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></span>
4955                          <span style="position:absolute; inset:0; margin:auto; width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,0.16); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:15px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span>
4956                            <span style="position:absolute; bottom:6px; left:8px; color:#fff; font-size:11px; font-weight:700;">Video {{ v.n }}</span>
4957                          </div>
4958                        </sc-if>
4959                      </div>
4960                      <!-- filled: cover toggle + poster row -->
4961                      <sc-if value="{{ v.filled }}" hint-placeholder-val="{{ false }}">
4962                        <div style="padding:9px 10px; background:#fafcfe;">
4963                         <span onClick="{{ v.setLotCover }}" style="display:flex;align-items:center;justify-content:center;gap:5px;text-align:center; font-size:10.5px; font-weight:700; padding:6px; border-radius:8px; cursor:pointer; background:{{ v.coverBg }}; color:{{ v.coverColor }}; border:1px solid {{ v.coverBorder }}; margin-bottom:8px; transition:all 150ms;"><sc-if value="{{ v.isLotCover }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if>{{ v.coverLabel }}</span>
4964                          <div style="display:flex; gap:6px;">
4965                            <sc-for list="{{ v.posterOpts }}" as="p" hint-placeholder-count="3">
4966                              <div onClick="{{ p.pick }}" style="position:relative; flex:1; min-width:0; aspect-ratio:16/9; border-radius:7px; overflow:hidden; cursor:pointer; outline:2px solid {{ p.ring }}; outline-offset:1px; transition:outline-color 150ms;">
4967                                <div style="position:absolute; inset:0; background-image:url('{{ p.url }}'); background-size:cover; background-position:center;"></div>
4968                                <sc-if value="{{ p.selected }}" hint-placeholder-val="{{ false }}">
4969                                  <span style="position:absolute; inset:0; background:rgba(10,92,168,0.3);"></span>
4970                              <span style="position:absolute; top:3px; right:3px; width:15px; height:15px; border-radius:50%; background:#0a5ca8; color:#fff; display:grid; place-items:center; font-size:9px; font-weight:800;"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></span>
4971                                </sc-if>
4972                              </div>
4973                            </sc-for>
4974                          </div>
4975                        </div>
4976                      </sc-if>
4977                      <sc-if value="{{ v.uploading }}" hint-placeholder-val="{{ false }}">
4978                        <div style="padding:9px 10px; background:#fafcfe; display:flex; gap:6px;">
4979                          <div style="flex:1; aspect-ratio:16/9; border-radius:7px; background:linear-gradient(90deg,#eef2f7 25%,#e2e8f0 37%,#eef2f7 63%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></div>
4980                          <div style="flex:1; aspect-ratio:16/9; border-radius:7px; background:linear-gradient(90deg,#eef2f7 25%,#e2e8f0 37%,#eef2f7 63%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></div>
4981                          <div style="flex:1; aspect-ratio:16/9; border-radius:7px; background:linear-gradient(90deg,#eef2f7 25%,#e2e8f0 37%,#eef2f7 63%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></div>
4982                        </div>
4983                      </sc-if>
4984                    </div>
4985                  </sc-for>
4986                </div>
4987              </sc-if>
4988            </div>
4989            <sc-if value="{{ formMessage }}" hint-placeholder-val="{{ false }}">
4990              <div style="margin-top:16px; font-size:13px; text-align:center; padding:11px; border-radius:10px; background:{{ formMsgBg }}; color:{{ formMsgColor }};">{{ formMessage }}</div>
4991            </sc-if>
4992            <div class="lot-wizard-footer" style="display:flex; justify-content:space-between; gap:12px; margin-top:22px; padding-top:18px; border-top:1px solid #eef3f9;">
4993              <button onClick="{{ prevStep }}" style="display:inline-flex;align-items:center;gap:6px;background:#eef3f9; color:#002e5f; border:1px solid #dde7f2; border-radius:12px; padding:12px 22px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer; visibility:{{ prevVisible }};" style-hover="background:#dde7f2;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Atrás</button>
4994              <button onClick="{{ nextStep }}" style="display:inline-flex;align-items:center;gap:7px;background:{{ nextBg }}; color:#fff; border:none; border-radius:12px; padding:12px 26px; font-size:14px; font-weight:800; font-family:inherit; cursor:pointer; transition:transform 160ms;" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.97);">{{ nextLabel }}<sc-if value="{{ nextShowsArrow }}" hint-placeholder-val="{{ true }}"><i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></sc-if></button>
4995            </div>
4996          </div>
4997          <!-- LIVE PREVIEW -->
4998          <sc-if value="{{ showPreviewCard }}" hint-placeholder-val="{{ true }}">
4999            <div style="position:{{ bidPanelPos }}; top:76px;">
5000              <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:10px;">PREVISUALIZACIÓN</div>
5001              <div style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 18px 50px rgba(0,30,60,0.14);">
5002                <div style="position:relative; height:180px; background:#001d3d;">
5003                  <div style="position:absolute; inset:0; background-image:url('{{ pv_image }}'); background-size:cover; background-position:center;"></div>
5004                  <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 40%, rgba(0,29,61,0.78));"></div>
5005                  <span style="position:absolute; top:10px; left:10px; background:rgba(0,29,61,0.82); color:#fff; font-size:11px; font-weight:700; padding:5px 10px; border-radius:8px;">CERT. {{ pv_cert }}</span>
5006                  <div style="position:absolute; bottom:10px; left:12px; right:12px; display:flex; justify-content:space-between; align-items:flex-end; gap:8px; color:#fff;">
5007                    <div><div style="font-size:16px; font-weight:800; line-height:1.15;">{{ pv_cat }}</div><div style="font-size:11.5px; color:#c8d8ea;">{{ pv_origen }}</div></div>
5008                    <div style="text-align:right; background:rgba(255,255,255,0.14); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.24); border-radius:10px; padding:6px 10px;"><div style="font-size:15px; font-weight:800; white-space:nowrap;">{{ pv_precio }}</div><div style="font-size:9.5px; color:#dbe6f2;">{{ pv_cond }}</div></div>
5009                  </div>
5010                </div>
5011                <div style="padding:14px 16px; display:flex; flex-direction:column; gap:9px;">
5012                  <div style="display:flex; gap:8px;">
5013                    <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033;">{{ pv_cabezas }}</div></div>
5014                    <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6;">PESO PROM.</div><div style="font-size:16px; font-weight:800; color:#112033;">{{ pv_peso }}</div></div>
5015                  </div>
5016                  <div style="display:flex; flex-direction:column; gap:5px; font-size:12.5px;">
5017                    <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Raza</span><span style="color:#112033; font-weight:600; text-align:right;">{{ pv_razas }}</span></div>
5018                    <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Destino</span><span style="color:#112033; font-weight:600;">{{ pv_destino }}</span></div>
5019                    <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Completitud</span><span style="color:#0a5ca8; font-weight:700;">{{ pv_completeness }}</span></div>
5020                  </div>
5021                  <sc-if value="{{ pv_china }}" hint-placeholder-val="{{ false }}"><span style="width:fit-content; display:inline-flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; color:#9d2235; background:#fdeef0; border:1px solid #f6ccd2; padding:4px 10px; border-radius:100px;"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="16" height="12" style="display:block; width:16px; height:12px; object-fit:cover; border-radius:2px;">Apto China</span></sc-if>
5022                </div>
5023              </div>
5024            </div>
5025          </sc-if>
5026        </div>
5027        <div style="height:40px;"></div>
5028      </div>
5029    </sc-if>
5030
5031
5032    <!-- ============ FERIA ? CREADOR DE REMATE ============ -->
5033    <sc-if class="advance-public-hidden" value="{{ isFeria }}" hint-placeholder-val="{{ false }}">
5034      <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1);">
5035        <style>
5036          .feria-result-screen{position:fixed;inset:0;z-index:90;box-sizing:border-box;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;background:linear-gradient(150deg,#001328,#002e5f 55%,#0a5ca8);display:flex;padding:clamp(16px,3vh,32px) max(16px,env(safe-area-inset-right)) max(20px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));}
5037          .feria-result-content{width:100%;max-width:560px;min-width:0;margin:auto;text-align:center;color:#fff;overflow-wrap:anywhere;}
5038          .feria-result-back{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:10px 14px;margin:0 0 16px;border:1px solid #ffffff50;border-radius:10px;background:#ffffff12;color:#fff;font:600 14px inherit;cursor:pointer;}
5039          .feria-result-back:focus-visible{outline:3px solid #fff;outline-offset:3px;}
5040          .feria-result-screen .feria-success-symbol{width:64px!important;height:64px!important;font-size:32px!important;margin-bottom:12px!important;}
5041          .feria-result-screen .feria-result-image{aspect-ratio:16/9;max-height:240px;min-height:130px;}
5042          .feria-result-actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:18px;}
5043          .feria-result-actions>a,.feria-result-actions>button{box-sizing:border-box;flex:1 1 160px;min-height:44px;justify-content:center;text-align:center;}
5044          .feria-result-data{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:16px;}
5045          @media(max-width:480px){.feria-result-screen h1{font-size:24px!important;margin-bottom:16px!important}.feria-result-data{grid-template-columns:1fr}.feria-result-actions>a,.feria-result-actions>button{flex-basis:100%}.feria-result-image strong{font-size:17px!important}.feria-result-body{padding:14px!important}}
5046          @media(max-height:600px){.feria-result-screen .feria-success-symbol{width:44px!important;height:44px!important;font-size:24px!important}.feria-result-screen .feria-result-image{max-height:170px}.feria-result-screen{padding-top:12px;}}
5047        </style>
5048        <!-- PROCESSING OVERLAY -->
5049        <sc-if value="{{ feriaProcessing }}" hint-placeholder-val="{{ false }}">
5050          <div class="feria-result-screen" role="region" aria-label="Estado de la transmisión">
5051            <div class="feria-result-content">
5052              <sc-if value="{{ feriaBusy }}" hint-placeholder-val="{{ true }}">
5053                <div style="width:120px; height:120px; margin:0 auto 26px; position:relative;">
5054                  <div style="position:absolute; inset:0; border-radius:50%; border:4px solid rgba(255,255,255,0.12);"></div>
5055                  <div style="position:absolute; inset:0; border-radius:50%; border:4px solid transparent; border-top-color:#fff; border-right-color:rgba(255,255,255,0.6); animation:spin 1s linear infinite;"></div>
5056                  <div style="position:absolute; inset:22px; border-radius:50%; background:rgba(255,255,255,0.08); display:grid; place-items:center; font-size:34px;"><i class="dc-lucide dc-lucide-radio-tower" aria-hidden="true"></i></div>
5057                  <span style="position:absolute; inset:0; border-radius:50%; box-shadow:0 0 0 0 rgba(255,255,255,0.4); animation:bidPulse 2s ease-out infinite;"></span>
5058                </div>
5059                <div style="display:flex; gap:8px; justify-content:center; margin-bottom:20px;">
5060                  <sc-for list="{{ feriaSteps }}" as="ps" hint-placeholder-count="3">
5061                   <div style="display:flex; align-items:center; gap:8px; font-size:12.5px; color:{{ ps.color }}; font-weight:600;"><span style="width:20px; height:20px; border-radius:50%; background:{{ ps.dotBg }}; color:{{ ps.dotColor }}; display:grid; place-items:center; font-size:11px;"><sc-if value="{{ ps.done }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if><sc-if value="{{ ps.numberVisible }}" hint-placeholder-val="{{ true }}">{{ ps.number }}</sc-if></span>{{ ps.label }}</div>
5062                  </sc-for>
5063                </div>
5064                <h1 style="font-size:clamp(24px, 3.4vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:0 0 8px;">{{ feriaPhaseTitle }}</h1>
5065                <p style="font-size:14.5px; color:#c8d8ea; margin:0; line-height:1.5; max-width:400px; margin:0 auto;">{{ feriaPhaseText }}</p>
5066              </sc-if>
5067              <sc-if value="{{ feriaDone }}" hint-placeholder-val="{{ false }}"><div style="text-align:left;"><button type="button" class="feria-result-back" onClick="{{ backFromFeriaResult }}"><span aria-hidden="true">←</span> Volver a mi perfil</button></div>
5068                <div class="feria-success-symbol" style="width:96px; height:96px; margin:0 auto 22px; border-radius:50%; background:rgba(31,214,143,0.16); border:2px solid #5fd68f; color:#38b977; display:grid; place-items:center; font-size:44px; animation:fadeScale 500ms cubic-bezier(0.22,0.72,0.18,1) both;"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></div>
5069                <div style="font-size:11.5px; letter-spacing:0.14em; color:#9fc0e2; font-weight:700;">TRANSMISIÓN CREADA</div>
5070                <h1 style="font-size:clamp(24px, 3.4vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:4px 0 22px;">Tu transmisión está lista</h1>
5071                <div style="background:#fff; border-radius:20px; overflow:hidden; box-shadow:0 30px 80px rgba(0,20,45,0.4); color:#112033; text-align:left; animation:floatIn 500ms cubic-bezier(0.22,0.72,0.18,1) 120ms both;">
5072                  <div class="feria-result-image" style="position:relative;">
5073                    <div style="position:absolute; inset:0; background-image:url('{{ resultThumb }}'); background-size:cover; background-position:center;"></div>
5074                    <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.04) 45%, rgba(0,29,61,0.78));"></div>
5075                    <span style="position:absolute; top:12px; left:12px; display:inline-flex; align-items:center; gap:7px; background:rgba(230,57,70,0.94); color:#fff; font-size:11.5px; font-weight:800; padding:6px 13px; border-radius:100px; letter-spacing:0.05em;"><span style="width:7px; height:7px; border-radius:50%; background:#fff; animation:pulseDot 1.2s infinite;"></span>EN VIVO</span>
5076                    <sc-if value="{{ resultHasLogo }}" hint-placeholder-val="{{ false }}"><span style="position:absolute; top:12px; right:12px; width:42px; height:42px; border-radius:11px; background:#fff; overflow:hidden;"><div style="width:100%; height:100%; background-image:url('{{ resultLogo }}'); background-size:contain; background-position:center; background-repeat:no-repeat; padding:5px; box-sizing:border-box;"></div></span></sc-if>
5077                    <strong style="position:absolute; bottom:12px; left:16px; right:16px; color:#fff; font-size:19px; text-shadow:0 1px 6px rgba(0,0,0,0.4);">{{ resultTitle }}</strong>
5078                  </div>
5079                  <div class="feria-result-body" style="padding:18px 20px;">
5080                    <div style="font-size:11px; color:#8593a6; letter-spacing:0.06em; font-weight:600;">ENLACE DE TRANSMISIÓN</div>
5081                    <div style="display:flex; gap:8px; margin-top:8px;">
5082                      <input value="" data-dc-attr-value="{{ resultUrl }}" readonly style="flex:1; min-width:0; border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:13px; font-family:inherit; color:#0a5ca8; background:#f6f9fc; outline:none;">
5083                      <button onClick="{{ copyResult }}" style="background:#002e5f; color:#fff; border:none; border-radius:10px; padding:11px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap;" style-hover="background:#0a5ca8;">Copiar</button>
5084                    </div>
5085                    <div class="feria-result-data">
5086                      <div style="background:#f6f9fc; border-radius:10px; padding:10px 13px;"><div style="font-size:10.5px; color:#8593a6;">CONSIGNATARIA</div><div style="font-size:13.5px; font-weight:700; color:#112033;">{{ resultCons }}</div></div>
5087                      <div style="background:#f6f9fc; border-radius:10px; padding:10px 13px;"><div style="font-size:10.5px; color:#8593a6;">FECHA</div><div style="font-size:13.5px; font-weight:700; color:#112033;">{{ resultDate }}</div></div>
5088                    </div>
5089                  </div>
5090                </div>
5091                <div class="feria-result-actions">
5092                  <a href="{{ resultUrl }}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:7px;background:#fff; color:#002e5f; padding:13px 26px; border-radius:12px; font-weight:700; font-size:14.5px;" style-hover="transform:translateY(-2px); color:#002e5f;">Abrir en YouTube <i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></a>
5093                  <button onClick="{{ shareFeriaFlyer }}" style="display:inline-flex; align-items:center; justify-content:center; gap:8px; background:#fff; color:#002e5f; border:0; padding:13px 22px; border-radius:12px; font-weight:700; font-size:14.5px; font-family:inherit; cursor:pointer;"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" style="display:block;flex-shrink:0;"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4m-6.8 7 6.8 4"/></svg><span>Compartir flyer</span></button>
5094                  <button onClick="{{ resetCreator }}" style="background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.3); color:#fff; padding:13px 26px; border-radius:12px; font-weight:700; font-size:14.5px; font-family:inherit; cursor:pointer;" style-hover="background:rgba(255,255,255,0.22);">Crear otra feria</button>
5095                </div>
5096              </sc-if>
5097              <sc-if value="{{ feriaFailed }}" hint-placeholder-val="{{ false }}">
5098                <div style="width:96px; height:96px; margin:0 auto 22px; border-radius:50%; background:rgba(230,57,70,0.16); border:2px solid #ff8f98; color:#e63946;display:grid; place-items:center; font-size:44px;"><i class="dc-lucide dc-lucide-circle-x" aria-hidden="true"></i></div>
5099                <h1 style="font-size:clamp(22px, 3vw, 28px); font-weight:800; margin:0 0 8px;">{{ feriaFailureTitle }}</h1>
5100                <p style="font-size:14px; color:#ffd3d7; margin:0 0 22px;">{{ feriaError }}</p>
5101                <sc-if value="{{ feriaCanRetry }}"><button onClick="{{ retryFeriaYoutube }}" style="background:#fff;color:#002e5f;padding:13px 26px;border-radius:12px;font-weight:700;margin:0 12px 12px 0;cursor:pointer;">{{ feriaRetryLabel }}</button><button onClick="{{ deleteFeria }}" disabled="{{ feriaDeleting }}" style="background:transparent;color:#ffd3d7;border:1px solid #ff8f98;padding:13px 26px;border-radius:12px;font-weight:700;margin:0 12px 12px 0;cursor:pointer;">{{ feriaDeleteLabel }}</button></sc-if>
5102                <button onClick="{{ resetCreator }}" style="background:#fff; color:#002e5f; padding:13px 26px; border-radius:12px; font-weight:700; font-size:14.5px; font-family:inherit; cursor:pointer;">Volver al formulario</button>
5103              </sc-if>
5104            </div>
5105          </div>
5106        </sc-if>
5107
5108        <!-- CREATOR -->
5109        <div style="padding:0 clamp(14px, 4vw, 36px); max-width:1200px; margin:0 auto;">
5110          <div style="padding:26px 0 18px;">
5111            <div style="font-size:11.5px; letter-spacing:0.14em; color:#1f7a63; font-weight:700;">ROSGAN FERIA</div>
5112            <h1 style="font-size:clamp(26px, 3.6vw, 38px); font-weight:800; letter-spacing:-0.025em; margin:4px 0 6px; color:#112033;">{{ feriaHeading }}</h1><sc-if value="{{ feriaEditingId }}"><button type="button" onClick="{{ cancelFeriaEdit }}" style="border:1px solid #ccdceb;border-radius:9px;background:#fff;color:#003767;padding:8px 12px;cursor:pointer;">Cancelar edición</button></sc-if>
5113            <p style="font-size:14.5px; color:#65758a; margin:0; max-width:560px; line-height:1.5;">Cargá los datos y generamos la transmisión en vivo de YouTube con su portada automáticamente.</p>
5114          </div>
5115          <sc-if value="{{ feriaDevicesAllowed }}">
5116            <div class="feria-control-actions">
5117              <button type="button" class="feria-control-button is-youtube" onClick="{{ c
5117ontrolFeriaYoutube }}"><img src="/assets/icons/youtube.svg" alt="" aria-hidden="true" width="22" height="22"><span>Controlar transmisiones de YouTube</span></button>
5118              <button type="button" class="feria-control-button" onClick="{{ goFeriaConfig }}"><img src="/assets/icons/lucide/sliders-horizontal.svg" alt="" aria-hidden="true" width="18" height="18"><span>Control de dispositivos</span><span aria-hidden="true">↗</span></button>
5119            </div>
5120          </sc-if>
5121          <div class="feria-creator-layout" style="display:grid; grid-template-columns:{{ feriaCols }}; gap:22px; align-items:start; padding-bottom:44px;">
5122            <!-- FORM -->
5123            <div class="feria-form-card" style="background:#fff; border-radius:22px; padding:clamp(20px, 2.8vw, 30px); box-shadow:0 14px 44px rgba(0,30,60,0.09); display:flex; flex-direction:column; gap:20px;">
5124              <div>
5125                <div style="font-size:12px; font-weight:700; color:#3d4c5f; margin-bottom:10px;">Consignataria *</div>
5126                 <div class="feria-consignataria-grid" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(130px, 1fr)); gap:10px;">
5127                   <sc-for list="{{ consChips }}" as="c" hint-placeholder-count="6">
5128                    <div onClick="{{ c.pick }}" style="display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:12px; cursor:{{ c.cursor }}; border:1.5px solid {{ c.border }}; background:{{ c.bg }}; transition:all 160ms;" style-hover="border-color:#1f7a63;">
5129                      <span style="width:32px; height:32px; border-radius:8px; background:{{ c.logoBg }}; overflow:hidden; flex-shrink:0; display:grid; place-items:center; padding:4px; box-sizing:border-box;"><img data-dc-attr-src="{{ c.whiteLogo }}" alt="" aria-hidden="true" style="display:block; width:100%; height:100%; object-fit:contain; filter:brightness(0) invert(1);"></span>
5130                      <span style="font-size:12px; font-weight:600; color:#112033; line-height:1.2; overflow:hidden;">{{ c.name }}</span>
5131                    </div>
5132                   </sc-for>
5133                 </div>
5134                <sc-if value="{{ feriaConsScopeText }}"><div style="margin-top:8px; color:{{ feriaConsScopeColor }}; font-size:11.5px; line-height:1.4;">{{ feriaConsScopeText }}</div></sc-if>
5135               </div>
5136              <sc-if value="{{ feriaHasConsignataria }}">
5137                <div class="feria-favorites" style="--feria-brand:{{ feriaFavoriteColor }};">
5138                  <strong style="font-size:13px; color:#153955;">Tus Ferias</strong>
5139                  <p style="font-size:12px; color:#65758a; margin:5px 0 12px;">{{ feriaFavoritesHint }}</p>
5140                  <style>
5141                    .feria-favorite-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}
5142                    .feria-favorite{position:relative;display:flex;align-items:center;gap:12px;text-align:left;padding:14px 12px;min-height:94px;border:1px solid #dce7ee;border-radius:16px;background:linear-gradient(135deg,#fff,#f1f7f8);color:#153955;font:inherit;cursor:pointer;transition:border-color .18s,box-shadow .18s}
5143                    .feria-favorite{background:linear-gradient(135deg,#fff,color-mix(in srgb,var(--feria-brand) 4%,white))}
5144                    .feria-favorite:hover{border-color:var(--feria-brand);box-shadow:0 5px 16px #123c4910}.feria-favorite:focus-visible{outline:3px solid #338dba;outline-offset:3px}
5145                    .feria-favorite[aria-pressed="true"]{border-color:var(--feria-brand);background:color-mix(in srgb,var(--feria-brand) 7%,white);box-shadow:inset 0 0 0 1px var(--feria-brand)}
5146                    .feria-favorite-emblem{position:relative;width:58px;height:58px;flex:0 0 58px}
5147                    .feria-favorite-map{display:block;width:100%;height:100%;background:var(--feria-brand);opacity:.65;mask:var(--feria-map) center/contain no-repeat;-webkit-mask:var(--feria-map) center/contain no-repeat}
5148                    .feria-favorite[aria-pressed="true"] .feria-favorite-map{opacity:1}
5149                    .feria-favorite-logo{position:absolute;right:-2px;bottom:-2px;display:grid;place-items:center;width:26px;height:26px;padding:3px;box-sizing:border-box;border-radius:9px;background:#fff;border:1px solid color-mix(in srgb,var(--feria-brand) 20%,white);box-shadow:0 2px 5px #11203314}
5150                    .feria-favorite-logo img{width:100%;height:100%;object-fit:contain}
5151                    .feria-favorite-copy{display:grid;gap:4px;min-width:0}.feria-favorite-copy strong{font-size:13px;line-height:1.3;overflow-wrap:anywhere}.feria-favorite-copy small{font-size:10px;color:#617d8c;line-height:1.3}.feria-favorite-check{position:absolute;right:8px;top:6px;color:var(--feria-brand);font-size:12px}
5152                    @media(max-width:400px){.feria-favorite-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.feria-favorite{gap:7px;padding:12px 9px}.feria-favorite-emblem{width:42px;height:48px;flex-basis:42px}.feria-favorite-logo{width:22px;height:22px;border-radius:7px}}
5153                  </style>
5154                  <div class="feria-favorite-grid">
5155                    <sc-for list="{{ feriaFavorites }}" as="f">
5156                      <button type="button" class="feria-favorite" onClick="{{ f.pick }}" aria-pressed="{{ f.selected }}">
5157                        <span class="feria-favorite-emblem" aria-hidden="true">
5158                          <span class="feria-favorite-map" style="--feria-map:url('{{ f.mapSrc }}');"></span>
5159                          <sc-if value="{{ feriaFavoriteLogo }}"><span class="feria-favorite-logo"><img src="{{ feriaFavoriteLogo }}" alt="" width="20" height="20" /></span></sc-if>
5160                        </span>
5161                        <span class="feria-favorite-copy"><strong>{{ f.localidad }}</strong><small>{{ f.provincia }}</small><sc-if value="{{ f.predio }}"><small>{{ f.predio }}</small></sc-if></span>
5162                        <sc-if value="{{ f.selected }}"><span class="feria-favorite-check" aria-hidden="true">✓</span></sc-if>
5163                      </button>
5164                    </sc-for>
5165                  </div>
5166                  <button type="button" onClick="{{ feriaManualLocation }}" style="margin-top:10px; padding:8px 0; border:0; background:transparent; color:#1f7a63; font:inherit; font-size:12px; font-weight:700; cursor:pointer;">Usar otra ubicación</button>
5167                </div>
5168              </sc-if>
5169              <sc-if value="{{ feriaManualOpen }}">
5170              <div style="display:grid;gap:20px;">
5171              <div class="feria-field-grid" style="display:grid; grid-template-columns:{{ feriaGrid2 }}; gap:14px;">
5172                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Provincia *</span>
5173                  <select value="" data-dc-attr-value="{{ ff_provincia }}" onChange="{{ ff_setProvincia }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; background:#fff;" style-focus="border-color:#1f7a63;">
5174                    <option value="">{{ provPlaceholder }}</option>
5175                    <sc-for list="{{ provinceOpts }}" as="p" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for>
5176                  </select></label>
5177                <label style="position:relative; display:flex; flex-direction:column; gap:6px;"><span style="display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; font-weight:600; color:#3d4c5f;"><span>Localidad *</span><small style="display:inline-flex; align-items:center; gap:4px; color:#718296; font-weight:600;"><img src="assets/google-logo.webp" alt="" style="width:12px; height:12px; object-fit:contain;">Google</small></span>
5178                  <input value="" data-dc-attr-value="{{ ff_localidad }}" onInput="{{ ff_setLocalidad }}" onFocus="{{ ff_openLocalidades }}" onBlur="{{ ff_closeLocalidades }}" disabled="{{ locDisabled }}" autocomplete="off" placeholder="{{ locPlaceholder }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 38px 11px 13px; font-size:14px; font-family:inherit; outline:none; background:{{ locBg }};" style-focus="border-color:#1f7a63;">
5179                  <sc-if value="{{ feriaPlaceLoading }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute; right:13px; top:38px; color:#1f7a63;"></i></sc-if>
5180                  <sc-if value="{{ feriaPlaceResultsOpen }}" hint-placeholder-val="{{ false }}"><div role="listbox" aria-label="Localidades sugeridas por Google" style="position:absolute; z-index:50; left:0; right:0; top:calc(100% + 5px); padding:5px; border:1px solid #d6e2ed; border-radius:11px; background:#fff; box-shadow:0 14px 30px rgba(0,35,70,.18); max-height:220px; overflow:auto;"><sc-for list="{{ feriaPlaceResults }}" as="p" hint-placeholder-count="4"><button type="button" role="option" onClick="{{ p.pick }}" style="width:100%; display:grid; gap:2px; border:0; border-radius:8px; background:#fff; padding:8px 9px; text-align:left; cursor:pointer;"><strong style="font-size:11.5px; color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px; color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if>
5181                </label>
5182              </div>
5183              <sc-if value="{{ feriaPlaceError }}" hint-placeholder-val="{{ false }}">
5183<div style="margin-top:-7px; color:#8a5a00; font-size:10.5px;">{{ feriaPlaceError }}</div></sc-if>
5184              <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Nombre del remate</span>
5185                <input value="" data-dc-attr-value="{{ ff_nombre }}" onInput="{{ ff_setNombre }}" placeholder="Se autocompleta con la localidad" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label>
5186              </div>
5187              </sc-if>
5188              <div class="feria-field-grid" style="display:grid; grid-template-columns:{{ feriaGrid3 }}; gap:14px;">
5189                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Fecha *</span>
5190                  <input type="date" value="" data-dc-attr-value="{{ ff_fecha }}" onInput="{{ ff_setFecha }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label>
5191                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Hora *</span>
5192                  <input type="time" value="" data-dc-attr-value="{{ ff_hora }}" onInput="{{ ff_setHora }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label>
5193                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Cabezas *</span>
5194                  <input type="number" min="0" value="" data-dc-attr-value="{{ ff_cabezas }}" onInput="{{ ff_setCabezas }}" placeholder="1200" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label>
5195              </div>
5196              <sc-if value="{{ feriaFormMessage }}" hint-placeholder-val="{{ false }}"><div style="font-size:13px; padding:11px 14px; border-radius:10px; background:rgba(230,57,70,0.1); color:#c22;">{{ feriaFormMessage }}</div></sc-if>
5197              <button onClick="{{ submitFeria }}" style="background:{{ feriaBtnBg }}; color:#fff; border:none; border-radius:14px; padding:16px; font-size:16px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; transition:transform 160ms; display:flex; align-items:center; justify-content:center; gap:10px;" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.98);"><span style="font-size:18px;"><i class="dc-lucide dc-lucide-radio-tower" aria-hidden="true"></i></span>{{ feriaSubmitLabel }}</button>
5198            </div>
5199            <!-- LIVE PREVIEW -->
5200            <div class="feria-preview-card" style="position:{{ feriaPreviewPos }}; top:20px; display:flex; flex-direction:column; gap:14px;">
5201              <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700; padding-left:2px;">VISTA PREVIA DE LA PORTADA DE YOUTUBE</div>
5202              <rosgan-feria-thumbnail auction="{{ feriaThumbnailData }}" style="display:block;aspect-ratio:16/9;border-radius:20px;overflow:hidden;box-shadow:0 20px 56px rgba(0,30,60,.15);"></rosgan-feria-thumbnail>
5203              <div style="background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
5204                <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;"><span style="font-size:12px; font-weight:700; color:#3d4c5f;">Datos completos</span><span style="font-size:13px; font-weight:800; color:#1f7a63;">{{ feriaPct }}</span></div>
5205                <div style="height:7px; border-radius:100px; background:#eef3f9; overflow:hidden;"><div style="height:100%; width:{{ feriaPct }}; border-radius:100px; background:linear-gradient(90deg,#1f7a63,#0a5ca8); transition:width 400ms cubic-bezier(0.22,0.72,0.18,1);"></div></div>
5206              </div>
5207              <sc-if value="{{ hasCreatedFerias }}" hint-placeholder-val="{{ false }}">
5208                <div style="background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
5209                  <div style="font-size:11.5px; letter-spacing:0.1em; color:#8593a6; font-weight:700; margin-bottom:10px;">CREADAS EN ESTA SESIÓN</div>
5210                  <div style="display:flex; flex-direction:column; gap:9px;">
5211                    <sc-for list="{{ createdFeriaList }}" as="cf" hint-placeholder-count="2">
5212                      <a href="{{ cf.href }}" target="_blank" rel="noopener" style="display:flex; align-items:center; gap:10px; text-decoration:none;">
5213                        <span style="width:9px; height:9px; border-radius:50%; background:#1f7a63; flex-shrink:0;"></span>
5214                        <span style="flex:1; min-width:0; font-size:13px; font-weight:600; color:#112033; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ cf.title }}</span>
5215                        <span style="display:inline-flex;align-items:center;gap:4px;font-size:12px; color:#0a5ca8; font-weight:600;">Ver <i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></span>
5216                      </a>
5217                    </sc-for>
5218                  </div>
5219                </div>
5220              </sc-if>
5221            </div>
5222          </div>
5223        </div>
5224      </div>
5225    </sc-if>
5226
5227
5228    <sc-if value="{{ isFeriaConfig }}" hint-placeholder-val="{{ false }}">
5229      <div style="max-width:1240px;margin:0 auto;padding:28px clamp(16px,4vw,36px) 56px;animation:pageIn 300ms ease;">
5230        <button onClick="{{ goFeria }}" style="border:none;background:transparent;padding:0;margin:0 0 20px;color:#64758a;font:600 13px inherit;cursor:pointer;">← Volver a Feria</button>
5231        <rosgan-feria-devices></rosgan-feria-devices>
5232      </div>
5233    </sc-if>
5234
5235    <!-- ============ PERFIL ? DASHBOARD DE MERCADO ============ -->
5236    <sc-if value="{{ isDevActivityPage }}"><rosgan-dev-activity-page></rosgan-dev-activity-page></sc-if>
5237    <sc-if value="{{ isProfileArea }}" hint-placeholder-val="{{ false }}">
5238      <div class="profile-area" style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;">
5239        <sc-if value="{{ profileDashboardVisible }}" hint-placeholder-val="{{ true }}">
5240        <!-- hero -->
5241        <div class="profile-hero" style="position:relative; overflow:hidden; border-radius:22px; margin:24px 0 18px; padding:clamp(20px, 3vw, 30px); background:linear-gradient(120deg, #001d3d 0%, #0a3e78 60%, #0a5ca8 100%); color:#fff; display:flex; align-items:center; gap:18px; flex-wrap:wrap;">
5242          <div style="position:absolute; right:-40px; top:-40px; width:220px; height:220px; border-radius:50%; background:radial-gradient(circle, rgba(255,255,255,0.1), transparent 70%);"></div>
5243          <span class="profile-hero-avatar" style="color:#fff; width:66px; height:66px; border-radius:50%; background:{{ userAvatarBg }}; border:1px solid {{ userAvatarBorder }}; display:grid; place-items:center; font-size:23px; font-weight:800; flex-shrink:0; z-index:1; overflow:hidden;">
5244            <sc-if value="{{ userHasConsignatariaAvatar }}" hint-placeholder-val="false"><img data-profile-avatar="true" data-dc-attr-src="{{ userConsignatariaAvatar }}" alt="{{ userConsignatariaName }}" onClick="{{ openProfilePhotoViewer }}" title="Ver foto completa" style="width:{{ userAvatarHeroSize }}; height:{{ userAvatarHeroSize }}; object-fit:{{ userAvatarObjectFit }}; object-position:center; cursor:{{ profileAvatarCursor }};"></sc-if>
5245            <sc-if value="{{ userShowsInitials }}" hint-placeholder-val="true">{{ userInitials }}</sc-if>
5246          </span>
5247          <div class="profile-identity" style="flex:1; min-width:180px; z-index:1;">
5248            <div class="profile-kicker" style="font-size:11px; letter-spacing:0.14em; color:#9fc0e2; font-weight:700;">{{ profileKicker }}</div>
5249            <h1 style="font-size:clamp(22px, 3vw, 30px); font-weight:800; letter-spacing:-0.02em; margin:3px 0 3px;">{{ profileName }}</h1>
5250            <sc-if value="{{ profileIsDev }}"><button type="button" onClick="{{ openDevActivity }}" style="margin:12px 0 6px;padding:11px 16px;border:1px solid #cbdbea;border-radius:10px;background:#073d70;color:white;font:inherit;cursor:pointer;">Panel Dev · Actividad y usuarios online</button><span style="display:block;margin:0 0 12px;font-size:12px;color:#c4def4;overflow-wrap:anywhere;">{{ profileCurrentVersionLabel }}</span></sc-if>
5251            <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:5px;">
5252              <span class="profile-email" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:#c8d8ea;"><img src="assets/icons/lucide/mail.svg" alt="" style="width:14px; height:14px; filter:brightness(0) invert(1); opacity:0.82;">{{ profileEmail }}</span>
5253              <sc-for list="{{ profileRoleBadges }}" as="role" hint-placeholder-count="1">
5254                <span style="display:inline-flex; align-items:center; gap:6px; padding:5px 9px; border-radius:100px; background:{{ role.badgeBg }}; border:1px solid {{ role.badgeBorder }}; color:{{ role.badgeColor }}; font-size:11px; font-weight:800;">
5255                  <img data-dc-attr-src="{{ role.icon }}" alt="" title="{{ role.iconTitle }}" style="width:15px; height:15px; padding:{{ role.iconPadding }}; border-radius:{{ role.iconRadius }}; background:{{ role.iconBg }}; object-fit:contain; filter:{{ role.iconFilter }};">{{ role.label }}
5256                </span>
5257              </sc-for>
5258            </div>
5259          </div>
5260          <div class="profile-actions" style="display:flex; gap:10px; z-index:1;">
5261            <sc-if value="{{ profileCanManageUsers }}" hint-placeholder-val="{{ false }}">
5262              <button class="profile-action {{ profileUsersSelected }}" data-dc-attr-aria-pressed="{{ adminUsersOpen }}" data-dc-attr-aria-expanded="{{ adminUsersOpen }}" onClick="{{ toggleAdminUsers }}" title="Administrar usuarios" style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.14); color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;">
5263                <img src="assets/icons/lucide/users-round.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Usuarios
5264              </button>
5265              <button class="profile-action {{ profileMailingSelected }}" data-dc-attr-aria-pressed="{{ mailingOpen }}" data-dc-attr-aria-expanded="{{ mailingOpen }}" onClick="{{ toggleMailing }}" title="Administrar mailing" style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.14); color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;">
5266                <img src="assets/icons/lucide/mails.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Mailing
5267              </button>
5268            </sc-if>
5269            <button class="profile-action {{ profileEditorSelected }}" data-dc-attr-aria-pressed="{{ profileEditorOpen }}" data-dc-attr-aria-expanded="{{ profileEditorOpen }}" onClick="{{ openProfileEditor }}" title="Editar perfil" style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.14); color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;">
5270              <img src="assets/icons/lucide/user-pen.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Editar perfil
5271            </button>
5272            <button class="profile-action" onClick="{{ enableNotifications }}" disabled="{{ notificationsBusy }}" title="{{ notificationsTitle }}" style="display:inline-flex; align-items:center; gap:7px; background:{{ notificationsBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:{{ notificationsCursor }}; opacity:{{ notificationsOpacity }};">
5273              <img src="assets/icons/lucide/bell.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ notificationsLabel }}
5274            </button>
5275            <button class="profile-action" onClick="{{ toggleWhatsAppNotifications }}" disabled="{{ whatsappNotificationsBusy }}" title="{{ whatsappNotificationsTitle }}" style="display:inline-flex; align-items:center; gap:7px; background:{{ whatsappNotificationsBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:{{ whatsappNotificationsCursor }}; opacity:{{ whatsappNotificationsOpacity }};">
5276              <img src="assets/icons/lucide/phone.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ whatsappNotificationsLabel }}
5277            </button>
5278            <sc-if value="{{ profileCanSell }}" hint-placeholder-val="{{ false }}"><button type="button" class="profile-action profile-action-load" onClick="{{ goLoad }}" style="display:inline-flex;align-items:center;justify-content:center;gap:7px;background:#fff;color:#003d73;border:1px solid #fff;padding:10px 18px;border-radius:12px;font-family:inherit;font-size:13.5px;font-weight:700;cursor:pointer;" style-hover="background:#e7f1fb;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i>Cargar lote</button></sc-if>
5279            <span class="profile-action profile-action-logout" onClick="{{ logout }}" style="background:#fff; color:#002e5f; padding:10px 18px; border-radius:12px; font-size:13.5px; font-weight:700; cursor:pointer;" style-hover="transform:translateY(-2px);">Salir</span>
5280          </div>
5281        </div>
5282
5283        <sc-if data-public-blocked-overlay value="{{ profileEditorOpen }}" hint-placeholder-val="{{ false }}">
5284          <div class="profile-editor-overlay" style="position:fixed; inset:0; z-index:120; background:rgba(0,20,45,0.58); backdrop-filter:blur(5px); display:grid; place-items:center; padding:18px; overflow:auto;">
5285            <section class="profile-editor-dialog" role="dialog" aria-modal="true" aria-label="Editar perfil" style="width:min(620px, 100%); background:#fff; border-radius:12px; box-shadow:0 24px 70px rgba(0,20,45,0.32); overflow:hidden; animation:floatIn 240ms both;">
5286              <header style="display:flex; align-items:center; justify-content:space-between; gap:16px; padding:18px 20px; border-bottom:1px solid #e6edf5;">
5287                <div><div style="font-size:10.5px; letter-spacing:0.13em; color:#0a5ca8; font-weight:800;">MI CUENTA</div><h2 style="font-size:21px; margin:3px 0 0; color:#112033;">Editar perfil</h2></div>
5288                <button onClick="{{ closeProfileEditor }}" disabled="{{ profileEditorSaving }}" title="Cerrar" style="width:36px; height:36px; border:1px solid #d7e2ee; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:18px; height:18px;"></button>
5289              </header>
5290              <div style="padding:20px; display:grid; gap:18px;">
5291                <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
5292                  <span style="width:78px; height:78px; border-radius:50%; background:#edf4fb; border:1px solid #d4e2ef; display:grid; place-items:center; overflow:hidden; color:#003b72; font-size:22px; font-weight:850; flex-shrink:0;">
5293                    <sc-if value="{{ profileEditorHasAvatar }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ profileEditorAvatar }}" alt="Foto de perfil" style="width:100%; height:100%; object-fit:cover;"></sc-if>
5294                    <sc-if value="{{ profileEditorShowsInitials }}" hint-placeholder-val="{{ true }}">{{ profileEditorInitials }}</sc-if>
5295                  </span>
5296                  <div style="display:grid; gap:6px;">
5297                    <label style="display:inline-flex; width:max-content; align-items:center; gap:7px; padding:9px 13px; border-radius:8px; background:#edf5fc; color:#00457f; border:1px solid #c9dced; font-size:12.5px; font-weight:750; cursor:pointer;">
5298                      <img src="assets/icons/lucide/camera.svg" alt="" style="width:16px; height:16px;">Cambiar foto
5299                      <input type="file" accept="image/png,image/jpeg,image/webp" onChange="{{ onProfilePhoto }}" style="display:none;">
5300                    </label>
5301                    <span style="font-size:11.5px; color:#748398;">PNG, JPG o WebP. Máximo 5 MB.</span>
5302                  </div>
5303                </div>
5304                <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:14px;">
5305                  <label class="cb-contract-field">CUIT / CUIL<input inputmode="numeric" maxlength="13" data-dc-attr-value="{{ profileEditorCuit }}" onInput="{{ onProfileCuit }}" placeholder="20-18347370-1"></label>
5306                  <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Nombre
5307                    <input value="" data-dc-attr-value="{{ profileEditorFirstName }}" onInput="{{ onProfileFirstName }}" autocomplete="given-name" placeholder="Tu nombre" style="height:42px; border:1px solid #cfdbe8; border-radius:8px; padding:0 12px; font:inherit; color:#112033; outline:none;">
5308                  </label>
5309                  <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Apellido
5310                    <input value="" data-dc-attr-value="{{ profileEditorLastName }}" onInput="{{ onProfileLastName }}" autocomplete="family-name" placeholder="Tu apellido" style="height:42px; border:1px solid #cfdbe8; border-radius:8px; padding:0 12px; font:inherit; color:#112033; outline:none;">
5311                  </label>
5312                  <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Teléfono verificado
5313                    <input value="" data-dc-attr-value="{{ profileEditorPhoneNumber }}" onInput="{{ onProfilePhoneNumber }}" disabled="{{ profilePhoneReadOnly }}" type="tel" autocomplete="tel" placeholder="Ej. +54 341 555 0000" style="height:42px; border:1px solid #cfdbe8; border-radius:8px; padding:0 12px; font:inherit; color:#112033; outline:none; background:{{ profilePhoneBg }};">
5314                   <small style="display:inline-flex;align-items:center;gap:5px;color:#19815f; font-weight:750;"><sc-if value="{{ profilePhoneVerified }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if>{{ profilePhoneStatusText }}</small>
5315                  </label>
5316                  <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Email
5317                    <input value="" data-dc-attr-value="{{ profileEmail }}" disabled style="height:42px; border:1px solid #dce5ee; border-radius:8px; padding:0 12px; font:inherit; color:#65758a; background:#f5f8fb;">
5318                  </label>
5319                </div>
5320                <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;"><span style="font-size:11.5px; color:#748398;">Roles asignados:</span><sc-for list="{{ profileRoleBadges }}" as="role" hint-placeholder-count="1"><span style="padding:4px 8px; border-radius:100px; background:#edf3f8; color:#40536a; font-size:10.5px; font-weight:750;">{{ role.label }}</span></sc-for></div>
5321                <sc-if value="{{ profileEditorHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:10px 12px; border-radius:8px; background:#fff0f1; color:#b42335; border:1px solid #f3c4ca; font-size:12.5px; font-weight:650;">{{ profileEditorError }}</div></sc-if>
5322              </div>
5323              <footer class="profile-editor-footer" style="display:flex; justify-content:flex-end; gap:10px; padding:15px 20px; border-top:1px solid #e6edf5; background:#f8fafc;">
5324                <button onClick="{{ closeProfileEditor }}" disabled="{{ profileEditorSaving }}" style="height:40px; padding:0 16px; border:1px solid #d3deea; border-radius:8px; background:#fff; color:#40536a; font:inherit; font-size:12.5px; font-weight:750; cursor:pointer;">Cancelar</button>
5325                <button onClick="{{ saveProfileEditor }}" disabled="{{ profileEditorSaving }}" style="height:40px; padding:0 17px; border:0; border-radius:8px; background:#0a5ca8; color:#fff; font:inherit; font-size:12.5px; font-weight:800; cursor:pointer; opacity:{{ profileEditorSaveOpacity }};">{{ profileEditorSaveLabel }}</button>
5326              </footer>
5327            </section>
5328          </div>
5329        </sc-if>
5330
5331        <sc-if data-public-blocked-overlay value="{{ profileCropOpen }}" hint-placeholder-val="{{ false }}">
5332          <div style="position:fixed; inset:0; z-index:150; background:rgba(0,20,45,0.68);
5332 backdrop-filter:blur(7px); display:grid; place-items:center; padding:18px; overflow:auto;">
5333            <section role="dialog" aria-modal="true" aria-label="Ajustar foto de perfil" style="width:min(520px, 100%); background:#fff; border-radius:12px; box-shadow:0 28px 80px rgba(0,20,45,0.38); overflow:hidden; animation:floatIn 220ms both;">
5334              <header style="display:flex; align-items:center; justify-content:space-between; gap:16px; padding:18px 20px; border-bottom:1px solid #e6edf5;">
5335                <div><div style="font-size:10.5px; letter-spacing:0.13em; color:#0a5ca8; font-weight:800;">FOTO DE PERFIL</div><h2 style="font-size:21px; margin:3px 0 0; color:#112033;">Ajustar foto</h2></div>
5336                <button onClick="{{ cancelProfileCrop }}" title="Cerrar" style="width:36px; height:36px; border:1px solid #d7e2ee; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:18px; height:18px;"></button>
5337              </header>
5338              <div style="padding:20px; display:grid; gap:18px;">
5339                <div style="display:grid; place-items:center; padding:8px 0 4px;">
5340                  <div style="width:min(286px, 74vw); aspect-ratio:1; border-radius:50%; overflow:hidden; border:4px solid #fff; outline:2px solid #0a5ca8; box-shadow:0 14px 38px rgba(0,48,90,0.2); background:#edf4fb;">
5341                    <canvas data-profile-crop-canvas="true" width="512" height="512" style="display:block; width:100%; height:100%;"></canvas>
5342                  </div>
5343                </div>
5344                <div style="display:grid; gap:13px;">
5345                  <label style="display:grid; grid-template-columns:20px 86px 1fr; align-items:center; gap:9px; color:#40536a; font-size:12px; font-weight:700;"><img src="assets/icons/lucide/zoom-in.svg" alt="" style="width:17px; height:17px;">Zoom<input type="range" min="1" max="3" step="0.05" value="" data-dc-attr-value="{{ profileCropZoom }}" onInput="{{ onProfileCropZoom }}" style="width:100%; accent-color:#0a5ca8;"></label>
5346                  <label style="display:grid; grid-template-columns:20px 86px 1fr; align-items:center; gap:9px; color:#40536a; font-size:12px; font-weight:700;"><img src="assets/icons/lucide/move-horizontal.svg" alt="" style="width:17px; height:17px;">Horizontal<input type="range" min="0" max="100" step="1" value="" data-dc-attr-value="{{ profileCropX }}" onInput="{{ onProfileCropX }}" style="width:100%; accent-color:#0a5ca8;"></label>
5347                  <label style="display:grid; grid-template-columns:20px 86px 1fr; align-items:center; gap:9px; color:#40536a; font-size:12px; font-weight:700;"><img src="assets/icons/lucide/move-vertical.svg" alt="" style="width:17px; height:17px;">Vertical<input type="range" min="0" max="100" step="1" value="" data-dc-attr-value="{{ profileCropY }}" onInput="{{ onProfileCropY }}" style="width:100%; accent-color:#0a5ca8;"></label>
5348                </div>
5349              </div>
5350              <footer style="display:flex; justify-content:flex-end; gap:10px; padding:15px 20px; border-top:1px solid #e6edf5; background:#f8fafc;">
5351                <button onClick="{{ cancelProfileCrop }}" style="height:40px; padding:0 16px; border:1px solid #d3deea; border-radius:8px; background:#fff; color:#40536a; font:inherit; font-size:12.5px; font-weight:750; cursor:pointer;">Cancelar</button>
5352                <button onClick="{{ confirmProfileCrop }}" style="height:40px; padding:0 17px; border:0; border-radius:8px; background:#0a5ca8; color:#fff; font:inherit; font-size:12.5px; font-weight:800; cursor:pointer; display:inline-flex; align-items:center; gap:7px;"><img src="assets/icons/lucide/check.svg" alt="" style="width:16px; height:16px; filter:brightness(0) invert(1);">Usar este recorte</button>
5353              </footer>
5354            </section>
5355          </div>
5356        </sc-if>
5357
5358        <sc-if data-public-blocked-overlay value="{{ adminUsersOpen }}" hint-placeholder-val="{{ false }}">
5359          <section data-reveal style="background:#fff; border-radius:18px; box-shadow:0 10px 32px rgba(0,30,60,0.09); overflow:hidden; margin-bottom:18px; border:1px solid #dce7f1;">
5360            <header style="display:flex; align-items:center; justify-content:space-between; gap:14px; padding:18px 20px; border-bottom:1px solid #e5edf5;">
5361              <div><div style="font-size:10.5px; letter-spacing:0.13em; color:#0a5ca8; font-weight:800;">ADMINISTRACI&Oacute;N</div><h2 style="font-size:20px; margin:3px 0 0; color:#112033;">Usuarios y roles</h2></div>
5362              <button onClick="{{ toggleAdminUsers }}" title="Cerrar panel" style="width:36px; height:36px; border:1px solid #d7e2ee; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:17px; height:17px;"></button>
5363            </header>
5364            <div style="display:flex;flex-wrap:wrap;gap:12px;padding:14px 18px;border-bottom:1px solid #e5edf5;background:#f8fafc;">
5365              <label style="flex:1 1 230px;min-width:0;display:grid;gap:5px;font-size:11px;font-weight:750;color:#60758c;">Usuarios<select aria-label="Grupo de usuarios" value="" data-dc-attr-value="{{ adminDirectoryGroup }}
5365" onChange="{{ onAdminDirectoryGroup }}" style="width:100%;min-width:0;height:42px;border:1px solid #d4e1ed;border-radius:10px;padding:0 10px;background:#fff;color:#123553;font:inherit;font-size:13px;"><sc-for list="{{ adminDirectoryFilters }}" as="filter"><option value="" data-dc-attr-value="{{ filter.id }}">{{ filter.label }} ({{ filter.count }})</option></sc-for></select></label>
5366              <div style="position:relative;align-self:flex-start;flex:1 1 250px;min-width:0;display:grid;gap:5px;font-size:11px;font-weight:750;color:#60758c;" onKeyDown="{{ onAdminCompanyKey }}"><span>Consignataria</span><button type="button" aria-label="Filtrar por consignataria" aria-expanded="{{ adminCompanyMenuExpanded }}" aria-controls="admin-company-options" onClick="{{ toggleAdminCompanyMenu }}" style="display:flex;align-items:center;gap:10px;width:100%;min-height:42px;border:1px solid #d4e1ed;border-radius:10px;padding:6px 12px;background:#fff;color:#123553;font:inherit;font-size:13px;text-align:left;cursor:pointer;"><sc-if value="{{ adminDirectoryCompanyHasLogo }}"><img data-dc-attr-src="{{ adminDirectoryCompanyLogo }}" alt="" style="width:28px;height:28px;object-fit:contain;"></sc-if><span style="flex:1;">{{ adminDirectoryCompanyLabel }}</span><span aria-hidden="true">⌄</span></button>
5367              <sc-if value="{{ adminCompanyMenuOpen }}"><div id="admin-company-options" aria-label="Consignatarias" style="position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;display:grid;gap:3px;max-height:280px;overflow:auto;padding:6px;border:1px solid #d4e1ed;border-radius:10px;background:#fff;box-shadow:0 12px 28px rgba(15,42,68,.16);"><sc-for list="{{ adminDirectoryCompanies }}" as="company"><button type="button" onClick="{{ company.select }}" aria-pressed="{{ company.selected }}" style="display:flex;align-items:center;gap:10px;min-height:44px;border:0;border-radius:7px;padding:7px 10px;background:{{ company.bg }};color:#123553;font:inherit;font-size:12px;text-align:left;cursor:pointer;"><sc-if value="{{ company.hasLogo }}"><img data-dc-attr-src="{{ company.logo }}" alt="" style="width:32px;height:30px;object-fit:contain;flex-shrink:0;"></sc-if><span style="flex:1;">{{ company.label }}</span><span style="font-variant-numeric:tabular-nums;">{{ company.count }}</span></button></sc-for></div></sc-if></div>
5368            </div>
5369            <sc-if value="{{ adminDirectoryCanSort }}"><label style="display:flex;align-items:center;gap:10px;padding:12px 18px;color:#60758c;font-size:12px;">Ordenar por<select aria-label="Orden de usuarios" data-dc-attr-value="{{ adminDirectorySort }}
5369" onChange="{{ onAdminDirectorySort }}" style="min-height:42px;border:1px solid #d4e1ed;border-radius:10px;padding:0 10px;background:#fff;color:#123553;font:inherit;"><option value="alphabetical">Nombre: A–Z</option><option value="newest">Registro: más recientes primero</option><option value="oldest">Registro: más antiguos primero</option></select></label></sc-if>
5370            <div style="display:grid; grid-template-columns:{{ adminUsersGridCols }}; min-height:360px;">
5371              <div style="padding:18px; border-right:{{ adminUsersDivider }}; border-bottom:{{ adminUsersMobileDivider }};">
5372                <label style="height:42px; display:flex; align-items:center; gap:9px; border:1px solid #cedbe8; border-radius:10px; padding:0 12px; background:#f9fbfd;">
5373                  <img src="assets/icons/lucide/search.svg" alt="" style="width:17px; height:17px; opacity:.65;">
5374                  <input value="" data-dc-attr-value="{{ adminUsersSearch }}" onInput="{{ onAdminUsersSearch }}" aria-label="Buscar usuarios" placeholder="Nombre, email o consignataria" style="min-width:0; flex:1; border:0; outline:0; background:transparent; color:#112033; font:inherit; font-size:13px;">
5375                </label>
5376                <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin:12px 0;font-size:12px;color:#60758c;"><span>{{ adminDirectorySummary }}</span><button type="button" onClick="{{ refreshAdminDirectory }}" disabled="{{ adminUsersLoading }}" style="border:1px solid #dce6ef;background:#fff;border-radius:8px;padding:7px 10px;color:#075a9c;cursor:pointer;">Actualizar</button></div>
5377                <sc-if value="{{ adminRecentMissingDates }}"><p style="font-size:12px;color:#60758c;">{{ adminMissingDateText }}</p></sc-if>
5378                <sc-if value="{{ adminUsersLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:34px 10px; text-align:center; color:#65758a; font-size:13px;">Cargando usuarios...</div></sc-if>
5379                <sc-if value="{{ adminUsersHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; padding:11px 12px; border-radius:9px; background:#fff0f1; border:1px solid #f2c7cc; color:#b42335; font-size:12px;">{{ adminUsersError }}</div></sc-if>
5380                <sc-if value="{{ adminUsersEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:34px 10px; text-align:center; color:#65758a; font-size:13px;">No encontramos usuarios.</div></sc-if>
5381                <div style="display:grid; gap:8px; margin-top:12px; max-height:560px; overflow:auto; padding-right:3px;">
5382                  <sc-for list="{{ adminUsersList }}" as="account" hint-placeholder-count="2">
5383                    <button onClick="{{ account.select }}" style="width:100%; display:flex; align-items:center; gap:11px; text-align:left; border:1px solid {{ account.border }}; background:{{ account.bg }}; border-radius:10px; padding:10px; cursor:pointer; font-family:inherit; color:#112033;">
5384                      <span style="width:38px; height:38px; border-radius:50%; overflow:hidden; display:grid; place-items:center; flex-shrink:0; background:#e8f2fb; color:#075a9c; font-weight:850;">
5385                        <sc-if value="{{ account.hasPhoto }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ account.photoUrl }}" alt="" style="width:100%; height:100%; object-fit:cover;"></sc-if>
5386                        <sc-if value="{{ account.showsInitials }}" hint-placeholder-val="{{ true }}">{{ account.initials }}</sc-if>
5387                      </span>
5388                      <span style="min-width:0; flex:1;"><strong style="display:block; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ account.displayName }}</strong><span style="display:block; margin-top:2px; font-size:11px; color:#66778c; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ account.email }}</span><span style="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;"><sc-for list="{{ account.roleBadges }}" as="badge"><span style="display:inline-flex;align-items:center;gap:4px;padding:3px 6px;border-radius:6px;background:{{ badge.bg }};color:{{ badge.color }};font-size:10px;font-weight:700;"><img data-dc-attr-src="{{ badge.icon }}" alt="" style="width:12px;height:12px;">{{ badge.label }}</span></sc-for><sc-for list="{{ account.companyBadges }}" as="company"><span style="display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:3px 6px;border-radius:6px;background:color-mix(in srgb, {{ company.color }} 10%, white);color:color-mix(in srgb, {{ company.color }} 75%, #17314d);font-size:10px;font-weight:700;"><sc-if value="{{ company.hasLogo }}"><img data-dc-attr-src="{{ company.logo }}" alt="" style="width:12px;height:12px;object-fit:contain;flex-shrink:0;"></sc-if><span style="overflow-wrap:anywhere;">{{ company.name }}</span></span></sc-for></span><span style="display:block;margin-top:4px;font-size:10px;color:#738397;">{{ account.registrationLabel }}</span><span class="admin-version-list"><sc-for list="{{ account.versionDevices }}" as="device"><span class="admin-version-item" style="background:{{ device.bg }};color:{{ device.color }};"><strong>{{ device.label }}</strong><small>{{ device.detail }}</small></span></sc-for></span></span>
5389
5390                    </button>
5391                  </sc-for>
5392                </div>
5393                <sc-if value="{{ adminDirectoryPaginated }}"><nav aria-label="Páginas de usuarios" style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:12px;margin-top:12px;border-top:1px solid #e5edf5;font-size:12px;color:#60758c;"><button type="button" onClick="{{ adminDirectoryPrevious }}" disabled="{{ adminDirectoryFirstPage }}" style="border:1px solid #d4e1ed;background:#fff;color:#123553;padding:9px 12px;border-radius:8px;
5393cursor:pointer;">Anterior</button><span>{{ adminDirectoryPageLabel }}</span><button type="button" onClick="{{ adminDirectoryNext }}" disabled="{{ adminDirectoryLastPage }}" style="border:1px solid #d4e1ed;background:#fff;color:#123553;padding:9px 12px;border-radius:8px;cursor:pointer;">Siguiente</button></nav></sc-if>
5394              </div>
5395              <div style="padding:20px;">
5396                <sc-if value="{{ adminUserNotSelected }}" hint-placeholder-val="{{ true }}"><div style="height:100%; min-height:280px; display:grid; place-items:center; text-align:center; color:#65758a;"><div><img src="assets/icons/lucide/user-cog.svg" alt="" style="width:34px; height:34px; opacity:.48;"><p style="margin:10px 0 0; font-size:13px;">Seleccion&aacute; una cuenta para administrar sus permisos.</p></div></div></sc-if>
5397                <sc-if value="{{ adminUserSelected }}" hint-placeholder-val="{{ false }}">
5398                  <div style="display:grid; gap:19px;">
5399                    <div><div style="font-size:10.5px; letter-spacing:.11em; color:#8090a5; font-weight:800;">CUENTA SELECCIONADA</div><h3 style="font-size:19px; margin:4px 0 2px; color:#112033;">{{ adminSelectedName }}</h3><div style="font-size:12px; color:#65758a;">{{ adminSelectedEmail }}</div></div>
5400                    <div><div style="font-size:11px; font-weight:800; color:#40536a; margin-bottom:8px;">Roles</div><div style="display:flex; flex-wrap:wrap; gap:8px;"><sc-for list="{{ adminRoleOptions }}" as="role" hint-placeholder-count="4"><button onClick="{{ role.toggle }}" style="display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px; border-radius:100px; border:1px solid {{ role.border }}; background:{{ role.bg }}; color:{{ role.color }}; font:inherit; font-size:11.5px; font-weight:800; cursor:pointer;"><img data-dc-attr-src="{{ role.icon }}" alt="" style="width:14px; height:14px; filter:{{ role.iconFilter }};">{{ role.label }}</button></sc-for></div></div>
5401                    <sc-if value="{{ adminNeedsConsignataria }}" hint-placeholder-val="{{ false }}">
5402                      <div><div style="font-size:11px; font-weight:800; color:#40536a; margin-bottom:4px;">Consignatarias</div><p style="font-size:11.5px; color:#748398; margin:0 0 9px;">Eleg&iacute; una o m&aacute;s consignatarias activas para esta cuenta.</p><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:8px;"><sc-for list="{{ adminConsignatariaOptions }}" as="cons" hint-placeholder-count="3"><button onClick="{{ cons.select }}" aria-pressed="{{ cons.selected }}" style="min-height:48px; display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:9px; border:1px solid {{ cons.border }}; background:{{ cons.bg }}; color:#17314d; font:inherit; font-size:11px; font-weight:750; cursor:pointer; text-align:left;"><span style="width:30px; height:30px; border-radius:50%; background:#fff; display:grid; place-items:center; flex-shrink:0; border:1px solid #dbe5ee;"><img data-dc-attr-src="{{ cons.logo }}" alt="" style="max-width:23px; max-height:23px; object-fit:contain;"></span>{{ cons.name }}</button></sc-for></div></div>
5403                    </sc-if>
5404                    <sc-if value="{{ adminRelationLoading }}" hint-placeholder-val="{{ false }}"><div style="font-size:12px; color:#65758a;">Consultando asignaciones...</div></sc-if>
5405                    <sc-if value="{{ adminUserHasMessage }}" hint-placeholder-val="{{ false }}"><div style="padding:10px 12px; border-radius:9px; background:{{ adminUserMessageBg }}; border:1px solid {{ adminUserMessageBorder }}; color:{{ adminUserMessageColor }}; font-size:12px; font-weight:650;">{{ adminUserMessageText }}</div></sc-if>
5406                    <div style="display:flex; justify-content:flex-end;"><button onClick="{{ saveAdminUser }}" disabled="{{ adminUserSaving }}" style="height:40px; display:inline-flex; align-items:center; gap:8px; padding:0 16px; border:0; border-radius:9px; background:#0a5ca8; color:#fff; font:inherit; font-size:12.5px; font-weight:800; cursor:pointer; opacity:{{ adminUserSaveOpacity }};"><img src="assets/icons/lucide/save.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ adminUserSaveLabel }}</button></div>
5407                  </div>
5408                </sc-if>
5409              </div>
5410            </div>
5411          </section>
5412        </sc-if>
5413        </sc-if>
5414
5415        <sc-if data-public-blocked-overlay value="{{ mailingOpen }}" hint-placeholder-val="{{ false }}">
5416          <section data-reveal style="background:#fff; border-radius:18px; box-shadow:0 10px 32px rgba(0,30,60,0.09); overflow:hidden; margin:24px 0 18px; border:1px solid #dce7f1;">
5417            <header style="display:flex; align-items:center; justify-content:space-between; gap:14px; padding:18px 20px; border-bottom:1px solid #e5edf5; background:linear-gradient(120deg,#001d3d,#075ca8); color:#fff;">
5418              <div><div style="font-size:10.5px; letter-spacing:.13em; color:#8fdce8; font-weight:800;">MAILGUN · ADMINISTRACI&Oacute;N</div><h2 style="font-size:21px; margin:3px 0 0;">Mailing Rosgan</h2><p style="font-size:12px; margin:5px 0 0; color:#c8d9e9;">Contactos, campa&ntilde;as, pruebas y env&iacute;os masivos desde [email protected].</p></div>
5419              <div style="display:flex; align-items:center; gap:10px;"><span style="padding:7px 10px; border-radius:100px; background:{{ mailingStatusBg }}; border:1px solid rgba(255,255,255,.25); font-size:11px; font-weight:800;">{{ mailingStatusLabel }}</span><button onClick="{{ toggleMailing }}" title="Cerrar panel" style="width:36px; height:36px; border:1px solid rgba(255,255,255,.28); border-radius:50%; background:rgba(255,255,255,.12); display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:17px; height:17px; filter:brightness(0) invert(1);"></button></div>
5420            </header>
5421            <sc-if value="{{ mailingLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:34px; text-align:center; color:#65758a; font-size:13px;">Cargando mailing...</div></sc-if>
5422            <sc-if value="{{ mailingHasError }}" hint-placeholder-val="{{ false }}"><div style="margin:16px 18px 0; padding:11px 13px; border-radius:9px; background:#fff0f1; border:1px solid #f2c7cc; color:#b42335; font-size:12px; font-weight:650;">{{ mailingError }}</div></sc-if>
5423            <sc-if value="{{ mailingHasMessage }}" hint-placeholder-val="{{ false }}"><div style="margin:16px 18px 0; padding:11px 13px; border-radius:9px; background:{{ mailingMessageBg }}; border:1px solid {{ mailingMessageBorder }}; color:{{ mailingMessageColor }}; font-size:12px; font-weight:650;">{{ mailingMessageText }}</div></sc-if>
5424            <div style="padding:18px; display:grid; grid-template-columns:{{ mailingGridCols }}; gap:16px;">
5425              <aside style="display:grid; align-content:start; gap:14px;">
5426                <article style="border:1px solid #dce7f1; border-radius:13px; padding:15px; background:#f8fbfd;">
5427                  <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;"><div aria-live="polite"><div style="font-size:10px; letter-spacing:.1em; color:#6a7d92; font-weight:800;">BASE DE CONTACTOS</div><strong style="display:block; margin-top:3px; font-size:25px; color:#07345f;">{{ mailingContactCount }}</strong><span style="font-size:11px; color:#6a7d92;">{{ mailingContactsLabel }}</span></div><span style="width:40px; height:40px; border-radius:12px; background:#e3f1fb; display:grid; place-items:center;"><img src="assets/icons/lucide/contact-round.svg" alt="" style="width:20px; height:20px;"></span></div>
5428                 <button type="button" onClick="{{ toggleMailingConsent }}" style="width:100%; margin-top:14px; padding:9px 10px; display:flex; align-items:flex-start; gap:9px; text-align:left; border:1px solid {{ mailingConsentBorder }}; border-radius:9px; background:{{ mailingConsentBg }}; color:#40536a; font:inherit; font-size:10.5px; line-height:1.45; cursor:pointer;"><span style="width:18px; height:18px; flex:0 0 18px; margin-top:1px; border-radius:5px; display:grid; place-items:center; background:{{ mailingConsentCheckBg }}; border:1px solid {{ mailingConsentBorder }}; color:#fff; font-weight:900;"><sc-if value="{{ mailingConsentChecked }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if></span><span>Confirmo que todos los contactos aceptaron recibir comunicaciones de Rosgan y que puedo acreditar ese consentimiento.</span></button>
5429                  <label style="margin-top:9px; height:40px; display:flex; align-items:center; justify-content:center; gap:8px; border-radius:9px; background:#075ca8; color:#fff; font-size:12px; font-weight:800; cursor:pointer; opacity:{{ mailingImportOpacity }};"><img src="assets/icons/lucide/file-up.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Importar CSV<input type="file" accept=".csv,text/csv" onChange="{{ importMailingCsv }}" disabled="{{ mailingImportDisabled }}" style="display:none;"></label>
5430                  <p style="font-size:10.5px; line-height:1.5;
5430 color:#77889b; margin:9px 0 0;">Columnas aceptadas: <strong>email</strong>, nombre, origen, estado, validaci&oacute;n y rebotes. Las bases grandes se cargan completas: el sistema divide el archivo autom&aacute;ticamente en bloques t&eacute;cnicos. La campa&ntilde;a incluye a todos los contactos suscriptos.</p>
5431                </article>
5432                <article style="border:1px solid #dce7f1; border-radius:13px; overflow:hidden;">
5433                  <div style="padding:12px 13px; background:#f4f8fb; border-bottom:1px solid #dce7f1; font-size:11px; font-weight:850; color:#40536a;">CAMPA&Ntilde;AS</div>
5434                  <sc-if value="{{ mailingCampaignsPending }}" hint-placeholder-val="{{ true }}"><div role="status" style="padding:18px 14px;color:#526b82;font-size:12px;">Consultando campañas…</div></sc-if>
5435                  <sc-if value="{{ mailingHasError }}" hint-placeholder-val="{{ false }}"><button onClick="{{ retryMailing }}" disabled="{{ mailingLoading }}" style="margin:10px;padding:8px 12px;border:1px solid #cbd8e5;border-radius:8px;background:white;color:#0a5ca8;">Reintentar consultas</button></sc-if>
5436                  <sc-if value="{{ mailingCampaignsEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:22px 14px; text-align:center; color:#7a8b9d; font-size:11.5px;">Todav&iacute;a no hay campa&ntilde;as.</div></sc-if>
5437                  <div style="max-height:310px; overflow:auto; padding:8px; display:grid; gap:7px;">
5438                    <sc-for list="{{ mailingCampaignList }}" as="campaign" hint-placeholder-count="2">
5439                      <div style="display:flex; align-items:stretch; border:1px solid {{ campaign.border }}; background:{{ campaign.bg }}; border-radius:9px; overflow:hidden;">
5440                        <button onClick="{{ campaign.select }}" style="min-width:0; flex:1; text-align:left; border:0; background:transparent; padding:10px; cursor:pointer; font-family:inherit;"><strong style="display:block; font-size:12px; color:#12314f; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ campaign.name }}</strong><span style="display:flex; justify-content:space-between; gap:8px; margin-top:4px; font-size:10px; color:#718397;"><span>{{ campaign.statusLabel }}</span><span>{{ campaign.progress }}</span></span><span style="display:block; margin-top:5px; font-size:9.5px; color:#526b82;">{{ campaign.audienceLabel }} · {{ campaign.sendMeta }}</span></button>
5441                        <button type="button" onClick="{{ campaign.remove }}" disabled="{{ campaign.deleteDisabled }}" title="Eliminar campaña" style="width:38px; flex:0 0 38px; border:0; border-left:1px solid #dce6ef; background:transparent; display:grid; place-items:center; cursor:pointer; opacity:{{ campaign.deleteOpacity }};"><img src="assets/icons/lucide/trash-2.svg" alt="Eliminar" style="width:15px; height:15px;"></button>
5442                      </div>
5443                    </sc-for>
5444                  </div>
5445                  <button onClick="{{ newMailingCampaign }}" style="width:calc(100% - 16px); height:36px; margin:0 8px 8px; border:1px dashed #86aacb; border-radius:8px; background:#fff; color:#075ca8; font:inherit; font-size:11.5px; font-weight:800; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nueva campa&ntilde;a</button>
5446                  <button onClick="{{ newLaunchMailingCampaign }}" style="width:calc(100% - 16px); min-height:44px; margin:0 8px 8px; padding:8px 11px; border:0; border-radius:10px; background:linear-gradient(120deg,#003b71,#0875a7 64%,#21aeb7); color:#fff; font:inherit; font-size:11.5px; line-height:1.25; font-weight:850; cursor:pointer; box-shadow:0 7px 17px rgba(0,67,116,.18);">Crear campa&ntilde;a de lanzamiento<br><span style="font-size:9.5px;font-weight:650;opacity:.82;">{{ mailingLaunchAudienceCount }}</span></button>
5447                </article>
5448              </aside>
5449              <div style="border:1px solid #dce7f1; border-radius:13px; padding:17px; display:grid; gap:13px; align-content:start;">
5450                <div style="display:flex; justify-content:space-between; align-items:center; gap:12px;"><div><div style="font-size:10px; letter-spacing:.1em; color:#6a7d92; font-weight:800;">COMPOSITOR</div><h3 style="font-size:18px; color:#12314f; margin:3px 0 0;">{{ mailingComposerTitle }}</h3></div><span style="padding:5px 9px; border-radius:100px; background:#edf3f8; color:#536b82; font-size:10px; font-weight:800;">{{ mailingDraftStatus }}</span></div>
5451                <div style="display:grid;gap:5px;font-size:11px;font-weight:750;color:#40536a;">Audiencia<div style="min-height:40px;
5451display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;border:1px solid {{ mailingAudienceBorder }};border-radius:8px;background:{{ mailingAudienceBg }};"><strong style="font-size:12px;color:{{ mailingAudienceColor }};">{{ mailingAudienceLabel }}</strong><span style="padding:4px 7px;border-radius:100px;background:#fff;border:1px solid {{ mailingAudienceBorder }};font-size:9px;color:{{ mailingAudienceColor }};white-space:nowrap;">Audiencia fija</span></div><small style="color:#788b9e;font-size:10px;font-weight:550;line-height:1.4;">{{ mailingAudienceHelp }}</small></div>
5452                <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:11px;">
5453                  <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Nombre interno<input value="" data-dc-attr-value="{{ mailingDraftName }}" onInput="{{ onMailingName }}" placeholder="Ej. Remate de septiembre" disabled="{{ mailingDraftLocked }}" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label>
5454                  <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Asunto<input value="" data-dc-attr-value="{{ mailingDraftSubject }}" onInput="{{ onMailingSubject }}" placeholder="Asunto del correo" disabled="{{ mailingDraftLocked }}" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label>
5455                </div>
5456                <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Texto previo<input value="" data-dc-attr-value="{{ mailingDraftPreview }}" onInput="{{ onMailingPreview }}" placeholder="Resumen corto que aparece junto al asunto" disabled="{{ mailingDraftLocked }}" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label>
5457                <div class="{{ mailingEditorShellClass }}" style="display:grid; gap:6px; font-size:11px; font-weight:750; color:#40536a;">
5458                  <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;"><span>Contenido del correo</span><div style="display:flex; gap:6px;"><button type="button" onClick="{{ previewMailingHtml }}" class="mailing-editor-tool" title="Vista previa del correo">Vista previa</button><button type="button" onClick="{{ toggleMailingSource }}" class="mailing-editor-tool" title="Alternar editor visual y HTML">{{ mailingEditorModeLabel }}</button><button type="button" onClick="{{ toggleMailingExpanded }}" class="mailing-editor-tool" title="Ampliar editor">{{ mailingEditorExpandLabel }}</button></div></div>
5459                  <sc-if value="{{ mailingEditorVisualMode }}" hint-placeholder-val="{{ true }}"><div class="mailing-editor-toolbar">
5460                    <button type="button" onMouseDown="{{ mailingEditorUndo }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Deshacer" aria-label="Deshacer"><span class="dc-lucide dc-lucide-undo" aria-hidden="true"></span></button><button type="button" onMouseDown="{{ mailingEditorRedo }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Rehacer" aria-label="Rehacer"><span class="dc-lucide dc-lucide-redo" aria-hidden="true"></span></button><span class="mailing-editor-separator"></span>
5461                    <select onChange="{{ mailingEditorFormat }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Formato de párrafo"><option value="p">P&aacute;rrafo</option><option value="h1">T&iacute;tulo 1</option><option value="h2">T&iacute;tulo 2</option><option value="h3">T&iacute;tulo 3</option><option value="blockquote">Cita</option></select><select onChange="{{ mailingEditorFont }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Tipografía"><option value="Arial">Arial</option><option value="Verdana">Verdana</option><option value="Georgia">Georgia</option><option value="Times New Roman">Times</option><option value="Courier New">Courier</option></select><select onChange="{{ mailingEditorFontSize }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Tamaño de texto"><option value="3">Normal</option><option value="2">Chico</option><option value="4">Grande</option><option value="5">Muy grande</option><option value="6">Destacado</option></select>
5462                    <button type="button" onMouseDown="{{ mailingEditorBold }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Negrita" aria-label="Negrita"><i class="dc-lucide dc-lucide-bold" aria-hidden="true"></i></button><button type="button" onMouseDown="{{ mailingEditorItalic }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Cursiva" aria-label="Cursiva"><i class="dc-lucide dc-lucide-italic" aria-hidden="true"></i></button><button type="button" onMouseDown="{{ mailingEditorUnderline }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Subrayado" aria-label="Subrayado"><i class="dc-lucide dc-lucide-underline" aria-hidden="true"></i></button>
5463                    <span class="mailing-editor-separator"></span><button type="button" onMouseDown="{{ mailingEditorAlignLeft }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Alinear a la izquierda">Izq.</button><button type="button" onMouseDown="{{ mailingEditorAlignCenter }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Centrar">Centro</button><button type="button" onMouseDown="{{ mailingEditorAlignRight }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Alinear a la derecha">Der.</button>
5464                    <span class="mailing-editor-separator"></span><button type="button" onMouseDown="{{ mailingEditorBullets }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Lista con viñetas">&bull; Lista</button><button type="button" onMouseDown="{{ mailingEditorNumbers }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Lista numerada">1. Lista</button>
5465                    <span class="mailing-editor-separator"></span><label class="mailing-editor-tool" title="Color de texto" style="display:flex;align-items:center;gap:5px;">Texto <input type="color" value="#173a5d" onInput="{{ mailingEditorColor }}" disabled="{{ mailingDraftLocked }}" style="width:20px;height:20px;border:0;padding:0;background:transparent;"></label><label class="mailing-editor-tool" title="Color de fondo" style="display:flex;align-items:center;gap:5px;">Fondo <input type="color" value="#fff2a8" onInput="{{ mailingEditorBackground }}" disabled="{{ mailingDraftLocked }}" style="width:20px;height:20px;border:0;padding:0;background:transparent;"></label>
5466                    <span class="mailing-editor-separator"></span><button type="button" onClick="{{ openMailingLink }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Enlace</button><button type="button" onClick="{{ openMailingButton }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Bot&oacute;n</button><button type="button" onClick="{{ openMailingImage }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Imagen</button><button type="button" onMouseDown="{{ mailingEditorDivider }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Separador</button><button type="button" onMouseDown="{{ mailingEditorName }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Nombre</button><button type="button" onMouseDown="{{ mailingEditorClear }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Limpiar formato</button>
5467                  </div></sc-if>
5468                  <sc-if value="{{ mailingEditorVisualMode }}" hint-placeholder-val="{{ true }}"><div data-mailing-rich-editor="true" contenteditable="{{ mailingEditorEditable }}" onInput="{{ onMailingVisualInput }}" onKeyUp="{{ saveMailingSelection }}" onMouseUp="{{ saveMailingSelection }}" onBlur="{{ saveMailingSelection }}" class="mailing-rich-editor" data-placeholder="Escribí el correo como en un procesador de textos. Podés agregar imágenes, enlaces y botones."></div></sc-if>
5469                  <sc-if value="{{ mailingEditorSourceMode }}" hint-placeholder-val="{{ false }}"><textarea value="" data-dc-attr-value="{{ mailingDraftHtml }}" onInput="{{ onMailingHtmlSource }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-source" spellcheck="false" placeholder="HTML del correo"></textarea></sc-if>
5470                  <small style="color:#7a8b9d;font-weight:500;line-height:1.45;">La firma institucional y la desuscripci&oacute;n se agregan autom&aacute;ticamente al enviar.</small>
5471                </div>
5472                <sc-if value="{{ mailingInsertDialogOpen }}" hint-placeholder-val="{{ false }}">
5473                  <div class="mailing-editor-dialog-backdrop">
5474                    <article class="mailing-editor-dialog">
5475                      <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;"><div><small style="color:#0a5ca8;font-weight:900;letter-spacing:.1em;">EDITOR DE CORREO</small><h3 style="margin:4px 0 0;color:#12314f;font-size:20px;">{{ mailingInsertDialogTitle }}</h3></div><button type="button" onClick="{{ closeMailingInsert }}" aria-label="Cerrar" title="Cerrar" style="width:36px;height:36px;display:grid;place-items:center;border:1px solid #d5e1ec;border-radius:50%;background:#fff;color:#12314f;cursor:pointer;"><span class="dc-lucide dc-lucide-x" aria-hidden="true"></span></button></div>
5476                      <div style="display:grid;gap:12px;margin-top:18px;">
5477                        <sc-if value="{{ mailingInsertIsImage }}" hint-placeholder-val="{{ false }}"><label style="height:76px;border:1px dashed #7ba8cc;border-radius:10px;background:#f1f8fd;color:#075ca8;display:grid;place-items:center;text-align:center;font-size:12px;font-weight:850;cursor:pointer;">{{ mailingImageUploadLabel }}<input type="file" accept="image/jpeg,image/png,image/webp,image/gif" onChange="{{ uploadMailingImage }}" disabled="{{ mailingImageUploading }}" style="display:none;"></label></sc-if>
5478                        <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">{{ mailingInsertUrlLabel }}<input value="" data-dc-attr-value="{{ mailingInsertUrl }}" onInput="{{ onMailingInsertUrl }}" placeholder="{{ mailingInsertUrlPlaceholder }}" style="height:42px;border:1px solid #cfdbe8;border-radius:8px;padding:0 11px;font:inherit;font-size:12px;"></label>
5479                        <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">{{ mailingInsertTextLabel }}<input value="" data-dc-attr-value="{{ mailingInsertText }}" onInput="{{ onMailingInsertText }}" placeholder="{{ mailingInsertTextPlaceholder }}" style="height:42px;border:1px solid #cfdbe8;border-radius:8px;padding:0 11px;font:inherit;font-size:12px;"></label>
5480                        <sc-if value="{{ mailingInsertIsImage }}" hint-placeholder-val="{{ false }}"><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;align-items:start;">
5481                          <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">Tama&ntilde;o<select value="" data-dc-attr-value="{{ mailingInsertImageSize }}" onChange="{{ onMailingImageSize }}" style="height:42px;border:1px solid #cfdbe8;border-radius:8px;padding:0 10px;background:#fff;color:#40536a;font:inherit;font-size:12px;"><option value="small">Peque&ntilde;a · 320 px</option><option value="medium">Mediana · 480 px</option><option value="large">Grande · 640 px</option><option value="full">Ancho completo</option></select></label>
5482                          <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">Enlace al hacer clic <span style="font-weight:500;color:#8493a3;">(opcional)</span><input value="" data-dc-attr-value="{{ mailingInsertImageLink }}" onInput="{{ onMailingImageLink }}" placeholder="https://sitio.com/destino" style="height:42px;border:1px solid {{ mailingImageLinkBorder }};border-radius:8px;padding:0 11px;font:inherit;font-size:12px;"><small style="min-height:14px;
5482color:{{ mailingImageLinkHintColor }};font-weight:550;">{{ mailingImageLinkHint }}</small></label>
5483                        </div></sc-if>
5484                        <sc-if value="{{ mailingInsertIsImage }}" hint-placeholder-val="{{ false }}"><sc-if value="{{ mailingImageHasPreview }}" hint-placeholder-val="{{ false }}"><div style="border:1px solid #dce6ef;border-radius:10px;padding:16px;background:#f8fbfd;text-align:center;overflow:auto;"><img data-dc-attr-src="{{ mailingInsertUrl }}" alt="Vista previa" style="display:block;width:100%;max-width:{{ mailingImagePreviewMaxWidth }};max-height:260px;height:auto;object-fit:contain;margin:auto;"></div></sc-if></sc-if>
5485                        <sc-if value="{{ mailingImageHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:9px 10px;border-radius:8px;background:#fff0f1;border:1px solid #f2c7cc;color:#b42335;font-size:11px;">{{ mailingImageError }}</div></sc-if>
5486                      </div>
5487                      <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:18px;"><button type="button" onClick="{{ closeMailingInsert }}" style="height:40px;padding:0 14px;border:1px solid #cbd8e5;border-radius:8px;background:#fff;color:#40536a;font-weight:800;cursor:pointer;">Cancelar</button><button type="button" onClick="{{ confirmMailingInsert }}" disabled="{{ mailingInsertDisabled }}" style="height:40px;padding:0 16px;border:0;border-radius:8px;background:#075ca8;color:#fff;font-weight:850;cursor:pointer;opacity:{{ mailingInsertOpacity }};">{{ mailingInsertConfirmLabel }}</button></div>
5488                    </article>
5489                  </div>
5490                </sc-if>
5491                <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Versi&oacute;n de texto <span style="font-weight:500; color:#8493a3;">(opcional)</span><textarea value="" data-dc-attr-value="{{ mailingDraftText }}" onInput="{{ onMailingText }}" placeholder="Alternativa sin HTML" disabled="{{ mailingDraftLocked }}" style="min-height:76px; resize:vertical; border:1px solid #cfdbe8; border-radius:8px; padding:11px; font:12px/1.5 inherit;"></textarea></label>
5492                <div style="display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:end;"><label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Email de prueba<input value="" data-dc-attr-value="{{ mailingTestEmail }}" onInput="{{ onMailingTestEmail }}" type="email" placeholder="[email protected]" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label><button onClick="{{ sendMailingTest }}" disabled="{{ mailingTestDisabled }}" style="height:40px; padding:0 13px; border:1px solid #a9bfd4; border-radius:8px; background:#fff; color:#075ca8; font:inherit; font-size:11.5px; font-weight:800; cursor:pointer; opacity:{{ mailingTestOpacity }};">Enviar prueba</button></div>
5493                <div style="display:flex; justify-content:flex-end; gap:9px; flex-wrap:wrap; padding-top:4px; border-top:1px solid #e8eef4;"><button onClick="{{ saveMailingDraft }}" disabled="{{ mailingSaveDisabled }}" style="height:40px; padding:0 15px; border:1px solid #b8cadd; border-radius:8px; background:#fff; color:#355069; font:inherit; font-size:12px; font-weight:800; cursor:pointer; opacity:{{ mailingSaveOpacity }};">Guardar campa&ntilde;a</button><button onClick="{{ queueMailingCampaign }}" disabled="{{ mailingQueueDisabled }}" style="height:40px; display:inline-flex; align-items:center; gap:7px; padding:0 16px; border:0; border-radius:8px; background:#0b7a59; color:#fff; font:inherit; font-size:12px; font-weight:850; cursor:pointer; opacity:{{ mailingQueueOpacity }};"><img src="assets/icons/lucide/send.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ mailingQueueLabel }}</button></div>
5494              </div>
5495            </div>
5496            <sc-if value="{{ mailingReportVisible }}" hint-placeholder-val="{{ false }}">
5497              <article style="margin:0 18px 18px; border:1px solid #dce7f1; border-radius:14px; overflow:hidden; background:#fff;">
5498                <header style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; padding:15px 17px; background:#f4f8fb; border-bottom:1px solid #dce7f1;">
5499                  <div><div style="font-size:10px; letter-spacing:.12em; color:#0a5ca8; font-weight:850;">TRACKING DE MARKETING</div><h3 style="margin:3px 0 0; font-size:17px; color:#12314f;">
5499Reporte por campa&ntilde;a</h3></div>
5500                  <div style="display:flex; gap:8px; flex-wrap:wrap;"><button onClick="{{ refreshMailingReport }}" disabled="{{ mailingReportLoading }}" style="height:36px; padding:0 12px; border:1px solid #b8cadd; border-radius:8px; background:#fff; color:#355069; font:inherit; font-size:11px; font-weight:800; cursor:pointer;">Actualizar</button><button onClick="{{ exportMailingReport }}" disabled="{{ mailingReportExportDisabled }}" style="height:36px; display:inline-flex; align-items:center; gap:7px; padding:0 13px; border:0; border-radius:8px; background:#075ca8; color:#fff; font:inherit; font-size:11px; font-weight:850; cursor:pointer; opacity:{{ mailingReportExportOpacity }};"><img src="assets/icons/lucide/download.svg" alt="" style="width:14px; height:14px; filter:brightness(0) invert(1);">{{ mailingReportExportLabel }}</button></div>
5501                </header>
5502                <sc-if value="{{ mailingReportLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:28px; text-align:center; color:#6c7f92; font-size:12px;">Actualizando m&eacute;tricas...</div></sc-if>
5503                <sc-if value="{{ mailingReportHasError }}" hint-placeholder-val="{{ false }}"><div style="margin:14px 16px 0; padding:10px 12px; border-radius:8px; background:#fff0f1; border:1px solid #f2c7cc; color:#b42335; font-size:11px;">{{ mailingReportError }}</div></sc-if>
5504                <sc-if value="{{ mailingReportReady }}" hint-placeholder-val="{{ false }}">
5505                  <div style="padding:16px; display:grid; gap:14px;">
5506                    <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:9px;">
5507                      <sc-for list="{{ mailingReportKpis }}" as="metric" hint-placeholder-count="6"><div style="border:1px solid {{ metric.border }}; background:{{ metric.bg }}; border-radius:10px; padding:11px 12px;"><span style="display:block; font-size:9px; letter-spacing:.08em; color:{{ metric.color }}; font-weight:850;">{{ metric.label }}</span><strong style="display:block; margin-top:4px; color:#12314f; font-size:21px;">{{ metric.value }}</strong><small style="display:block; margin-top:2px; color:#6d8094; font-size:9.5px;">{{ metric.detail }}</small></div></sc-for>
5508                    </div>
5509                    <div style="display:flex; gap:7px; flex-wrap:wrap;"><sc-for list="{{ mailingReportRates }}" as="rate" hint-placeholder-count="5"><span style="padding:6px 9px; border-radius:999px; border:1px solid #d4e1ec; background:#f8fbfd; color:#49627a; font-size:10px; font-weight:750;"><strong style="color:#075ca8;">{{ rate.value }}</strong> {{ rate.label }}</span></sc-for></div>
5510                    <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:9px; align-items:center;">
5511                      <label style="position:relative; display:block;"><img src="assets/icons/lucide/search.svg" alt="" style="position:absolute; left:11px; top:50%; width:15px; height:15px; transform:translateY(-50%); opacity:.55;"><input type="search" value="" data-dc-attr-value="{{ mailingReportSearch }}" onInput="{{ onMailingReportSearch }}" placeholder="Buscar por email o nombre" style="width:100%; height:40px; box-sizing:border-box; border:1px solid #cbd9e6; border-radius:9px; padding:0 12px 0 34px; background:#fff; color:#183650; font:inherit; font-size:11.5px;"></label>
5512                      <select value="" data-dc-attr-value="{{ mailingReportStatusFilter }}" onChange="{{ onMailingReportStatus }}" style="width:100%; height:40px; border:1px solid #cbd9e6; border-radius:9px; padding:0 11px; background:#fff; color:#355069; font:inherit; font-size:11px; font-weight:750;"><option value="all">Todos los estados</option><option value="accepted">Aceptados</option><option value="delivered">Entregados</option><option value="opened">Abiertos</option><option value="clicked">Con clic</option><option value="temporary_fail">Rebote temporal</option><option value="permanent_fail">Rebote definitivo</option><option value="unsubscribed">Desuscriptos</option><option value="complained">Quejas</option></select>
5513                    </div>
5514                    <div style="display:flex; align-items:center; gap:5px; overflow-x:auto; padding:2px 0 5px; scrollbar-width:thin;" aria-label="Filtrar contactos por inicial"><sc-for list="{{ mailingReportLetters }}" as="letter" hint-placeholder-count="28"><button type="button" onClick="{{ letter.select }}" disabled="{{ letter.disabled }}" title="{{ letter.count }} contactos" style="flex:0 0 auto; min-width:30px; height:30px; padding:0 8px; border:1px solid {{ letter.border }}; border-radius:8px; background:{{ letter.bg }}; color:{{ letter.color }}; opacity:{{ letter.opacity }}; font:inherit; font-size:10px; font-weight:850; cursor:pointer;">{{ letter.label }}</button></sc-for></div>
5515                    <div style="overflow:auto; border:1px solid #e0e8f0; border-radius:10px; max-height:390px;">
5516                      <table style="width:100%; min-width:980px; border-collapse:collapse; font-size:10.5px;">
5517                        <thead style="position:sticky; top:0; z-index:1; background:#edf3f8;"><tr><th style="padding:9px 10px; text-align:left;">Contacto</th><th style="padding:9px 10px; text-align:left;">Tipo</th><th style="padding:9px 10px; text-align:left;">Estado</th><th style="padding:9px 10px; text-align:left;">Enviado</th><th style="padding:9px 10px; text-align:left;">Entregado</th><th style="padding:9px 10px; text-align:left;">Aperturas</th><th style="padding:9px 10px; text-align:left;">Clics</th><th style="padding:9px 10px; text-align:left;">Detalle</th></tr></thead>
5518                        <tbody><sc-for list="{{ mailingReportRows }}" as="row" hint-placeholder-count="4"><tr style="border-top:1px solid #e7edf3;"><td style="padding:9px 10px;"><strong style="display:block; color:#153450;">{{ row.email }}</strong><span style="color:#718397;">{{ row.name }}</span></td><td style="padding:9px 10px;"><span style="display:inline-block; padding:4px 7px; border-radius:999px; background:{{ row.kindBg }}; color:{{ row.kindColor }}; font-size:9px; font-weight:850;">{{ row.kindLabel }}</span></td><td style="padding:9px 10px;"><span style="display:inline-block; padding:4px 7px; border-radius:999px; background:{{ row.statusBg }}; color:{{ row.statusColor }}; font-size:9px; font-weight:850;">{{ row.statusLabel }}</span></td><td style="padding:9px 10px; color:#52687d;">{{ row.sentAt }}</td><td style="padding:9px 10px; color:#52687d;">{{ row.deliveredAt }}</td><td style="padding:9px 10px; color:#52687d;"><strong>{{ row.openCount }}</strong><br><small>{{ row.openedAt }}</small></td><td style="padding:9px 10px; color:#52687d;"><strong>{{ row.clickCount }}</strong><br><small>{{ row.clickedAt }}</small></td><td style="padding:9px 10px; color:#7a5160; max-width:260px; white-space:normal;">{{ row.error }}</td></tr></sc-for></tbody>
5519                      </table>
5520                      <sc-if value="{{ mailingReportRowsEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:30px 18px; text-align:center; color:#718397; font-size:11px;">No hay env&iacute;os que coincidan con la b&uacute;squeda y los filtros.</div></sc-if>
5521                    </div>
5522                    <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;"><div style="font-size:10px; color:#718397;">Mostrando {{ mailingReportRange }} de {{ mailingReportFilteredTotal }} resultados filtrados &middot; {{ mailingReportTotal }} env&iacute;os registrados.</div><div style="display:flex; align-items:center; gap:7px;"><button type="button" onClick="{{ mailingReportPrev }}" disabled="{{ mailingReportPrevDisabled }}" style="height:32px; padding:0 11px; border:1px solid #cbd9e6; border-radius:8px; background:#fff; color:#355069; opacity:{{ mailingReportPrevOpacity }}; font:inherit; font-size:10px; font-weight:800; cursor:pointer;">Anterior</button><span style="min-width:88px; text-align:center; color:#52687d; font-size:10px; font-weight:750;">{{ mailingReportPageLabel }}</span><button type="button" onClick="{{ mailingReportNext }}" disabled="{{ mailingReportNextDisabled }}" style="height:32px; padding:0 11px; border:1px solid #cbd9e6; border-radius:8px; background:#fff; color:#355069; opacity:{{ mailingReportNextOpacity }}; font:inherit; font-size:10px; font-weight:800; cursor:pointer;">Siguiente</button></div></div>
5523                  </div>
5524                </sc-if>
5525              </article>
5526            </sc-if>
5527          </section>
5528        </sc-if>
5529
5530        <sc-if value="{{ profileActivityVisible }}" hint-placeholder-val="{{ false }}">
5531        <sc-if value="{{ profileRoslotVisible }}" hint-placeholder-val="{{ false }}">
5532        <sc-if value="{{ showBuyerCreditPanel }}" hint-placeholder-val="{{ false }}">
5533          <section data-reveal class="credit-live-panel">
5534            <div class="credit-live-head">
5535              <div class="credit-live-title"><div><div class="credit-live-eyebrow">MI CR&Eacute;DITO</div><h3>Crédito de compra</h3></div><span class="credit-live-status">EN TIEMPO REAL</span></div>
5536              <button type="button" onClick="{{ buyerCreditHistory }}" class="credit-live-history"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true"></i> Historial</button>
5537            </div>
5538            <sc-if value="{{ buyerCreditLoading }}" hint-placeholder-val="{{ false }}">
5539              <div style="display:flex; align-items:center; justify-content:center; gap:10px; padding:30px 20px; border-top:1px solid #f0f4f9; color:#65758a; font-size:13.5px; font-weight:600;"><span style="width:18px; height:18px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin 0.8s linear infinite;"></span>Actualizando crédito...</div>
5540            </sc-if>
5541            <sc-if value="{{ buyerCreditHas }}" hint-placeholder-val="{{ false }}">
5542              <sc-for list="{{ buyerCreditRows }}" as="c" hint-placeholder-count="2">
5543                <div class="credit-live-row">
5544                  <div class="credit-live-row-head">
5545                    <div class="credit-live-party"><strong>{{ c.seller }}</strong><span>{{ c.periodLabel }}</span></div>
5546                    <button type="button" onClick="{{ c.openHistory }}" disabled="{{ c.summaryDisabled }}" class="credit-live-history" style="opacity:{{ c.summaryOpacity }};">Anterior</button>
5547                  </div>
5548                  <sc-if value="{{ c.inlineLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; gap:8px; margin-top:14px; padding:15px; border-radius:12px; background:#f7fafc; color:#65758a; font-size:12px; font-weight:700;"><span style="width:16px; height:16px; border:2px solid #d8e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>Cargando actividad del crédito vigente...</div></sc-if>
5549                  <sc-if value="{{ c.inlineHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:14px; padding:13px 14px; border:1px solid #f0c7cc; border-radius:12px; background:#fff5f6; color:#9d2837; font-size:12px;"><strong>No se pudo cargar la actividad vigente.</strong><button type="button" onClick="{{ c.refreshInline }}" style="margin-left:10px; border:0; background:transparent; color:#075ca8; font:800 11px inherit; cursor:pointer;">Reintentar</button></div></sc-if>
5550                  <sc-if value="{{ c.inlineReady }}" hint-placeholder-val="{{ false }}">
5551                    <div class="credit-live-metrics"><div class="credit-live-metric is-primary"><small>DISPONIBLE</small><strong>{{ c.inlineAvailable }}</strong></div><div class="credit-live-metric is-used"><small>USADO</small><strong>{{ c.inlineUsed }}</strong></div><div class="credit-live-metric"><small>AUTORIZADO</small><strong>{{ c.inlineAuthorized }}</strong></div></div>
5552                    <div class="credit-live-progress" title="{{ c.inlineProgressLabel }} utilizado"><span style="width:{{ c.inlineProgress }};"></span></div>
5553                    <sc-if value="{{ c.inlineHasMovements }}" hint-placeholder-val="{{ false }}"><div class="credit-live-movements"><sc-for list="{{ c.inlineMovements }}" as="movement" hint-placeholder-count="3"><article class="credit-live-movement"><div><strong>{{ movement.lot }} · {{ movement.statusLabel }}</strong><small>{{ movement.date }}</small></div><b>{{ movement.consumedAmount }}</b></article></sc-for></div></sc-if>
5554                    <sc-if value="{{ c.inlineEmpty }}" hint-placeholder-val="{{ false }}"><div style="margin-top:8px; color:#718397; font-size:10.5px;">Sin movimientos en esta vigencia.</div></sc-if>
5555                  </sc-if>
5556                </div>
5557              </sc-for>
5558            </sc-if>
5559            <sc-if value="{{ buyerCreditEmpty }}" hint-placeholder-val="{{ false }}">
5560              <div style="padding:28px 20px; border-top:1px solid #f0f4f9; text-align:center;"><div style="font-size:15px; font-weight:800; color:#112033;">No ten&eacute;s crédito asignado</div><div style="font-size:13px; color:#65758a; margin-top:4px;">Cuando un vendedor te autorice, vas a ver ac&aacute; el monto semanal y la vigencia del crédito.</div></div>
5561            </sc-if>
5562          </section>
5563        </sc-if>
5564
5565        <sc-if value="{{ creditHistoryVisible }}" hint-placeholder-val="{{ false }}">
5566          <div onClick="{{ closeCreditHistoryBackdrop }}" style="position:fixed; inset:0; z-index:11990; display:flex; align-items:center; justify-content:center; padding:18px; background:rgba(0,32,63,.58); backdrop-filter:blur(8px); animation:fadeIn 180ms ease both;">
5567            <section onClick="{{ stopCreditHistoryClick }}" role="dialog" aria-modal="true" aria-label="Histórico de créditos" style="width:min(820p
5567x,100%); max-height:min(760px,calc(100vh - 36px)); overflow:auto; background:#fff; border:1px solid #dbe7f2; border-radius:22px; box-shadow:0 28px 80px rgba(0,25,55,.28); animation:floatIn 260ms cubic-bezier(.22,.72,.18,1) both;">
5568              <header style="position:sticky; top:0; z-index:2; display:flex; justify-content:space-between; align-items:flex-start; gap:14px; padding:20px 22px; background:linear-gradient(135deg,#002e5f,#0a65ad); color:#fff;">
5569                <div style="min-width:0;"><div style="font-size:10px; letter-spacing:.14em; font-weight:900; color:#8bd7ff;">HISTÓRICO DE CRÉDITOS · ROSLOT</div><h3 style="margin:4px 0 0; font-size:clamp(20px,3vw,27px); line-height:1.12;">{{ creditHistoryTitle }}</h3><div style="margin-top:6px; color:#d8eafa; font-size:12.5px;">Créditos finalizados o desactivados. El crédito vigente permanece visible en el perfil.</div></div>
5570                <button type="button" onClick="{{ closeCreditHistory }}" aria-label="Cerrar" style="width:38px; height:38px; flex:0 0 auto; display:grid; place-items:center; border:1px solid rgba(255,255,255,.3); border-radius:11px; background:rgba(255,255,255,.12); color:#fff; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
5571              </header>
5572              <div style="padding:18px 20px 22px;">
5573                <sc-if value="{{ creditHistoryHas }}" hint-placeholder-val="{{ false }}"><div style="display:flex; flex-direction:column; gap:9px;"><sc-for list="{{ creditHistoryRows }}" as="history" hint-placeholder-count="4"><article style="display:grid; grid-template-columns:repeat(auto-fit,minmax(135px,1fr)); gap:10px; align-items:center; padding:13px 14px; border:1px solid #e0e9f2; border-radius:13px; background:#fff;"><div style="min-width:0;"><span style="display:block; color:#718397; font-size:8.5px; font-weight:900; letter-spacing:.08em;">{{ creditHistoryPartyLabel }}</span><strong style="display:block; margin-top:3px; color:#173652; font-size:12.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ history.party }}</strong></div><div><span style="display:block; color:#718397; font-size:8.5px; font-weight:900; letter-spacing:.08em;">VIGENCIA</span><strong style="display:block; margin-top:3px; color:#173652; font-size:11.5px;">{{ history.period }}</strong></div><div><span style="display:block; color:#718397; font-size:8.5px; font-weight:900; letter-spacing:.08em;">CRÉDITO SEMANAL</span><strong style="display:block; margin-top:3px; color:#075ca8; font-size:14px;">{{ history.amount }}</strong></div><div style="display:flex; align-items:center; justify-content:flex-end; gap:8px; flex-wrap:wrap;"><span style="padding:5px 8px; border-radius:999px; background:{{ history.statusBg }}; color:{{ history.statusColor }}; font-size:9px; font-weight:900;">{{ history.statusLabel }}</span><button type="button" onClick="{{ history.openDetail }}" disabled="{{ history.detailDisabled }}" style="min-height:34px; border:1px solid #bfd3e7; border-radius:9px; background:#fff; color:#075ca8; padding:7px 10px; font:800 10.5px inherit; cursor:pointer; opacity:{{ history.detailOpacity }};">Ver detalle</button></div></article></sc-for></div></sc-if>
5574                <sc-if value="{{ creditHistoryEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:42px 20px; border:1px dashed #cad8e6; border-radius:14px; text-align:center; color:#6e8194;"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true" style="font-size:24px; color:#0a5ca8;"></i><strong style="display:block; margin-top:10px; color:#183750; font-size:15px;">Todavía no hay créditos anteriores</strong><span style="display:block; margin-top:4px; font-size:12px;">Cuando finalice una vigencia, aparecerá en este histórico.</span></div></sc-if>
5575              </div>
5576            </section>
5577          </div>
5578        </sc-if>
5579
5580        <sc-if value="{{ creditSummaryVisible }}" hint-placeholder-val="{{ false }}">
5581          <div onClick="{{ closeCreditSummaryBackdrop }}" style="position:fixed; inset:0; z-index:12000; display:flex; align-items:center; justify-content:center; padding:18px; background:rgba(0,32,63,.58); backdrop-filter:blur(8px); animation:fadeIn 180ms ease both;">
5582            <section onClick="{{ stopCreditSummaryClick }}" role="dialog" aria-modal="true" aria-label="Historial de crédito" style="width:min(920px,100%);
5582 max-height:min(820px,calc(100vh - 36px)); overflow:auto; background:#fff; border:1px solid #dbe7f2; border-radius:22px; box-shadow:0 28px 80px rgba(0,25,55,.28); animation:floatIn 260ms cubic-bezier(.22,.72,.18,1) both;">
5583              <header style="position:sticky; top:0; z-index:2; display:flex; justify-content:space-between; align-items:flex-start; gap:14px; padding:20px 22px; background:linear-gradient(135deg,#002e5f,#0a65ad); color:#fff;">
5584                <div style="min-width:0;"><div style="font-size:10px; letter-spacing:.14em; font-weight:900; color:#8bd7ff;">HISTORIAL DE CRÉDITO · ROSLOT</div><h3 style="margin:4px 0 0; font-size:clamp(20px,3vw,27px); line-height:1.12;">{{ creditSummaryTitle }}</h3><div style="margin-top:6px; color:#d8eafa; font-size:12.5px;">{{ creditSummarySubtitle }}</div></div>
5585                <button type="button" onClick="{{ closeCreditSummary }}" aria-label="Cerrar" style="width:38px; height:38px; flex:0 0 auto; display:grid; place-items:center; border:1px solid rgba(255,255,255,.3); border-radius:11px; background:rgba(255,255,255,.12); color:#fff; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
5586              </header>
5587              <div style="padding:20px 22px 24px;">
5588                <sc-if value="{{ creditSummaryLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; justify-content:center; gap:10px; min-height:230px; color:#65758a; font-size:13px; font-weight:700;"><span style="width:20px; height:20px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>Cargando movimientos...</div></sc-if>
5589                <sc-if value="{{ creditSummaryHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:18px; border:1px solid #f0c7cc; border-radius:14px; background:#fff4f5; color:#a12636;"><strong style="display:block;">No se pudo cargar el historial</strong><span style="display:block; margin-top:4px; font-size:12px;">{{ creditSummaryError }}</span><button type="button" onClick="{{ refreshCreditSummary }}" style="margin-top:12px; border:1px solid #d9aeb5; border-radius:9px; background:#fff; color:#a12636; padding:8px 12px; font:800 11px inherit; cursor:pointer;">Reintentar</button></div></sc-if>
5590                <sc-if value="{{ creditSummaryReady }}" hint-placeholder-val="{{ false }}">
5591                  <div style="display:grid; grid-template-columns:{{ creditSummaryStatsCols }}; gap:10px;">
5592                    <sc-for list="{{ creditSummaryStats }}" as="stat" hint-placeholder-count="3"><div style="padding:14px 15px; border:1px solid {{ stat.border }}; border-radius:14px; background:{{ stat.bg }};"><span style="display:block; font-size:9.5px; letter-spacing:.09em; color:{{ stat.color }}; font-weight:900;">{{ stat.label }}</span><strong style="display:block; margin-top:6px; color:#102f4c; font-size:clamp(20px,3vw,27px); font-variant-numeric:tabular-nums;">{{ stat.value }}</strong></div></sc-for>
5593                  </div>
5594                  <div style="margin-top:15px; padding:14px 15px; border:1px solid #dfe9f3; border-radius:14px; background:#f8fbfd;"><div style="display:flex; justify-content:space-between; gap:12px; margin-bottom:8px; color:#536a80; font-size:11px; font-weight:800;"><span>CRÉDITO UTILIZADO</span><span>{{ creditSummaryProgressLabel }}</span></div><div style="height:10px; overflow:hidden; border-radius:999px; background:#dce7f1;"><div style="width:{{ creditSummaryProgress }}; height:100%; border-radius:inherit; background:linear-gradient(90deg,#0a5ca8,#1f9b7a); transition:width 500ms cubic-bezier(.22,.72,.18,1);"></div></div></div>
5595                  <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin:22px 0 10px;"><div><div style="font-size:10px; letter-spacing:.12em; color:#0a5ca8; font-weight:900;">MOVIMIENTOS</div><h4 style="margin:3px 0 0; color:#112033; font-size:17px;">Actividad del crédito</h4></div><button type="button" onClick="{{ refreshCreditSummary }}" style="height:36px; border:1px solid #cbdbea; border-radius:9px; background:#fff; color:#075ca8; padding:0 12px; font:800 10.5px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-refresh-cw" aria-hidden="true"></i> Actualizar</button></div>
5596                  <sc-if value="{{ creditSummaryHasMovements }}" hint-placeholder-val="{{ false }}"><div style="display:flex; flex-direction:column; gap:8px;"><sc-for list="{{ creditSummaryMovements }}" as="movement" hint-placeholder-count="4"><article style="display:grid; grid-template-columns:{{ creditMovementCols }}; gap:10px; align-items:center; padding:12px 13px; border:1px solid #e1eaf3; border-radius:12px; background:#fff;"><div style="min-width:0;"><strong style="display:block; color:#163651; font-size:12.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ movement.lot }}</strong><span style="display:block; margin-top:3px; color:#7a8c9e; font-size:10.5px;">{{ movement.date }}</span></div><span style="justify-self:start; padding:5px 8px; border-radius:999px; background:{{ movement.statusBg }}; color:{{ movement.statusColor }}; font-size:9.5px; font-weight:900;">{{ movement.statusLabel }}</span><div><span style="display:block; color:#7a8c9e; font-size:9px; font-weight:800;">IMPORTE OFERTADO</span><strong style="display:block; margin-top:3px; color:#12314f; font-size:13px;">{{ movement.bidAmount }}</strong></div><div><span style="display:block; color:#7a8c9e; font-size:9px; font-weight:800;">CRÉDITO CONSUMIDO</span><strong style="display:block; margin-top:3px; color:#075ca8; font-size:13px;">{{ movement.consumedAmount }}</strong></div></article></sc-for></div></sc-if>
5597                  <sc-if value="{{ creditSummaryEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:34px 18px; border:1px dashed #cad8e6; border-radius:14px; text-align:center; color:#6e8194;"><strong style="display:block; color:#183750;">Todavía no hay movimientos</strong><span style="display:block; margin-top:4px; font-size:12px;">Las ofertas vigentes y los lotes ganados aparecerán acá.</span></div></sc-if>
5598                </sc-if>
5599              </div>
5600            </section>
5601          </div>
5602        </sc-if>
5603
5604        <sc-if value="{{ showLiveOffersPanel }}" hint-placeholder-val="{{ true }}">
5605          <div data-reveal style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden; margin-bottom:16px;">
5606            <div style="display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; padding:16px 20px 12px;">
5607              <div><div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">MIS OFERTAS</div><h3 style="font-size:19px; font-weight:800; margin:3px 0 0; color:#112033;">Seguimiento en vivo</h3></div>
5608              <span style="display:inline-flex; align-items:center; gap:7px; font-size:11px; font-weight:800; color:{{ livePanelStatusColor }}; background:{{ livePanelStatusBg }}; padding:6px 12px; border-radius:100px;"><span style="width:7px; height:7px; border-radius:50%; background:{{ livePanelStatusColor }}; animation:{{ livePanelDotAnimation }};"></span>{{ livePanelStatus }}</span>
5609            </div>
5610            <sc-for list="{{ liveOffers }}" as="o" hint-placeholder-count="3">
5611              <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #f0f4f9; flex-wrap:wrap;">
5612                <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer;"></div>
5613                <div onClick="{{ o.open }}" style="flex:1; min-width:130px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} &middot; {{ o.origen }}</div></div>
5614                <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">MI OFERTA</div><div style="font-size:16px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.myRoll }}</div></div>
5615                <div style="text-align:right; min-width:96px;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">MEJOR OFERTA</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ o.marketRoll }}</div></div>
5616                <span style="display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:800; padding:6px 12px; border-radius:100px; background:{{ o.standBg }}; color:{{ o.standColor }};">
5617                  <sc-if value="{{ o.winning }}" hint-placeholder-val="{{ true }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg></sc-if>
5618                  <sc-if value="{{ o.showOutbidIcon }}" hint-placeholder-val="{{ false }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"></polyline><polyline points="15 17 21 17 21 11"></polyline></svg></sc-if>
5619                  {{ o.standLabel }}
5620                </span>
5621                <sc-if value="{{ o.outbid }}" hint-placeholder-val="{{ false }}">
5622                  <div style="display:flex; align-items:center; gap:8px; background:#f6f9fc; border:1px solid #e6edf5; border-radius:12px; padding:6px 8px;">
5623                    <div style="display:flex; gap:4px;"><sc-for list="{{ o.incOpts }}" as="inc" hint-placeholder-count="2"><span onClick="{{ inc.pick }}" style="font-size:11px; font-weight:800; padding:5px 9px; border-radius:8px; cursor:pointer; background:{{ inc.activeBg }}; color:{{ inc.activeColor }}; border:1px solid {{ inc.activeBorder }};">{{ inc.label }}</span></sc-for></div>
5624                    <div style="display:flex; align-items:center; gap:6px;">
5625                      <span onClick="{{ o.minus }}" style="width:26px; height:26px; border-radius:8px; background:#fff; border:1px solid #dde7f2; color:#c22; display:grid; place-items:center; font-size:16px; font-weight:800; cursor:pointer; user-select:none;" style-active="transform:scale(0.9);"><i class="dc-lucide dc-lucide-minus" aria-hidden="true"></i></span>
5626                      <span style="min-width:58px; text-align:center; font-size:13px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.offsetLabel }}</span>
5627                      <span onClick="{{ o.plus }}" style="width:26px; height:26px; border-radius:8px; background:#fff; border:1px solid #dde7f2; color:#1f7a63; display:grid; place-items:center; font-size:16px; font-weight:800; cursor:pointer; user-select:none;" style-active="transform:scale(0.9);"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>
5628                    </div>
5629                    <button onClick="{{ o.confirm }}" disabled="{{ o.submitting }}" style="display:inline-flex; align-items:center; gap:6px; background:#e63946; color:#fff; border:none; border-radius:9px; padding:8px 14px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer; white-space:nowrap; opacity:{{ o.submitOpacity }};" style-hover="background:#cf2f3c;" style-active="transform:scale(0.96);">{{ o.confirmLabel }}</button>
5630                  </div>
5631                </sc-if>
5632              </div>
5633            </sc-for>
5634            <sc-if value="{{ buyerClosedHas }}" hint-placeholder-val="{{ false }}">
5635              <details style="border-top:1px solid #e5edf5; background:#f8fafc;">
5636                <summary style="display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; color:#3d4c5f; font-size:13px; font-weight:800; cursor:pointer; user-select:none; list-style:none;">
5637                  <span>Ofertas cerradas</span><span style="background:#e8eef5; color:#65758a; border-radius:100px; padding:4px 9px;">{{ buyerClosedCount }}</span>
5638                </summary>
5639                <sc-for list="{{ buyerClosedOfferRows }}" as="o" hint-placeholder-count="2">
5640                  <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #e8eef5; flex-wrap:wrap; background:#fff;">
5641                    <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer; filter:saturate(0.75);"></div>
5642                    <span style="font-size:10.5px; font-weight:800; padding:6px 10px; border-radius:100px; color:#65758a; background:#e8eef5;">OFERTA CERRADA</span>
5643                    <div onClick="{{ o.open }}" style="flex:1; min-width:150px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} &middot; {{ o.origen }}</div></div>
5644                    <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.amountLabel }}</div><div style="font-size:16px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div>
5645                    <span style="font-size:11px; font-weight:800; padding:6px 12px; border-radius:100px; background:{{ o.resultBg }}; color:{{ o.resultColor }};">{{ o.resultLabel }}</span>
5646                  </div>
5647                </sc-for>
5648                <sc-if value="{{ buyerClosedPaginated }}" hint-placeholder-val="{{ false }}">
5649                  <div style="display:flex; align-items:center; justify-content:center; gap:12px; padding:14px 20px; border-top:1px solid #e8eef5;">
5650                    <button onClick="{{ buyerClosedPrev }}" disabled="{{ buyerClosedPrevDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Anterior</button>
5651                    <span style="font-size:12px; color:#65758a; font-weight:700;">{{ buyerClosedPageLabel }}</span>
5652                    <button onClick="{{ buyerClosedNext }}" disabled="{{ buyerClosedNextDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Siguiente</button>
5653                  </div>
5654                </sc-if>
5655              </details>
5656            </sc-if>
5657            <sc-if value="{{ liveOffersLoading }}" hint-placeholder-val="{{ false }}">
5658              <div style="display:flex; align-items:center; justify-content:center; gap:10px; padding:28px 20px; border-top:1px solid #f0f4f9; color:#65758a; font-size:13.5px; font-weight:600;">
5659                <span style="width:18px; height:18px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin 0.8s linear infinite;"></span>
5660                Actualizando ofertas...
5661              </div>
5662            </sc-if>
5663            <sc-if value="{{ liveOffersEmpty }}" hint-placeholder-val="{{ false }}">
5664              <div style="padding:30px 20px; border-top:1px solid #f0f4f9; text-align:center;">
5665                <div style="font-size:15px; font-weight:800; color:#112033;">No tenés ofertas activas</div>
5666                <div style="font-size:13px; color:#65758a; margin-top:4px;">Cuando ofertes, vas a ver acá si seguís ganando o si te superaron.</div>
5667              </div>
5668            </sc-if>
5669          </div>
5670        </sc-if>
5671
5672        <sc-if value="{{ showSellerOffersPanel }}" hint-placeholder-val="{{ false }}">
5673          <div id="seller-offers-panel" data-reveal style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden; margin-bottom:16px;">
5674            <div style="display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; padding:16px 20px 12px;">
5675              <div><div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">OFERTAS RECIBIDAS</div><h3 style="font-size:19px; font-weight:800; margin:3px 0 0; color:#112033;">Seguimiento de mis lotes</h3></div>
5676              <span style="display:inline-flex; align-items:center; gap:7px; font-size:11px; font-weight:800; color:{{ sellerOffersStatusColor }}; background:{{ sellerOffersStatusBg }}; padding:6px 12px; border-radius:100px;"><span style="width:7px; height:7px; border-radius:50%; background:{{ sellerOffersStatusColor }}; animation:{{ sellerOffersStatusAnimation }};"></span>{{ sellerOffersStatus }}</span>
5677            </div>
5678            <sc-for list="{{ sellerOpenOfferRows }}" as="o" hint-placeholder-count="2">
5679              <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #f0f4f9; flex-wrap:wrap;">
5680                <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer;"></div>
5681                <span style="display:inline-flex; align-items:center; gap:6px; font-size:10.5px; font-weight:800; padding:6px 10px; border-radius:100px; color:#1f7a63; background:rgba(31,122,99,0.12);"><span style="width:6px; height:6px; border-radius:50%; background:#1f7a63; animation:pulseDot 1.2s infinite;"></span>OFERTA ABIERTA</span>
5682                <div onClick="{{ o.open }}" style="flex:1; min-width:170px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} &middot; {{ o.origen }}</div></div>
5683                <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">BASE</div><div style="font-size:15px; font-weight:800; color:#3d4c5f; font-variant-numeric:tabular-nums;">{{ o.base }}</div></div>
5684                <div style="min-width:130px;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.buyerLabel }}</div><div style="font-size:13px; font-weight:700; color:#112033;">{{ o.buyer }}</div></div>
5685                <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.amountLabel }}</div><div style="font-size:17px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div>
5686               <span style="display:inline-flex;align-items:center;gap:4px;font-size:11px; font-weight:800; padding:6px 10px; border-radius:100px; color:{{ o.variationColor }}; background:{{ o.variationBg }};"><sc-if value="{{ o.variationUp }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-up" aria-hidden="true"></i></sc-if><sc-if value="{{ o.variationDown }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-down" aria-hidden="true"></i></sc-if>{{ o.variation }}</span>
5687                <span style="font-size:11px; font-weight:800; padding:6px 11px; border-radius:100px; background:#eef4fb; color:#0a5ca8;">{{ o.count }}</span>
5688                <button onClick="{{ o.open }}" style="background:#002e5f; color:#fff; border:none; border-radius:9px; padding:9px 14px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer;">Ver ofertas</button>
5689              </div>
5690            </sc-for>
5691            <sc-if value="{{ sellerOffersLoading }}" hint-placeholder-val="{{ false }}">
5692              <div style="display:flex; align-items:center; justify-content:center; gap:10px; padding:28px 20px; border-top:1px solid #f0f4f9; color:#65758a; font-size:13.5px; font-weight:600;"><span style="width:18px; height:18px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin 0.8s linear infinite;"></span>Actualizando ofertas recibidas...</div>
5693            </sc-if>
5694            <sc-if value="{{ sellerOpenOffersEmpty }}" hint-placeholder-val="{{ false }}">
5695              <div style="padding:24px 20px; border-top:1px solid #f0f4f9; text-align:center;"><div style="font-size:15px; font-weight:800; color:#112033;">No hay ofertas abiertas en este momento</div><div style="font-size:13px; color:#65758a; margin-top:4px;">Las nuevas ofertas aparecerán acá automáticamente.</div></div>
5696            </sc-if>
5697            <sc-if value="{{ sellerClosedHas }}" hint-placeholder-val="{{ false }}">
5698              <details style="border-top:1px solid #e5edf5; background:#f8fafc;">
5699                <summary style="display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; color:#3d4c5f; font-size:13px; font-weight:800; cursor:pointer; user-select:none; list-style:none;">
5700                  <span>Ofertas cerradas</span><span style="background:#e8eef5; color:#65758a; border-radius:100px; padding:4px 9px;">{{ sellerClosedCount }}</span>
5701                </summary>
5702                <sc-for list="{{ sellerClosedOfferRows }}" as="o" hint-placeholder-count="2">
5703                  <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #e8eef5; flex-wrap:wrap; background:#fff;">
5704                    <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer; filter:saturate(0.75);"></div>
5705                    <span style="font-size:10.5px; font-weight:800; padding:6px 10px; border-radius:100px; color:#65758a; background:#e8eef5;">OFERTA CERRADA</span>
5706                    <div onClick="{{ o.open }}" style="flex:1; min-width:170px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} &middot; {{ o.origen }}</div></div>
5707                    <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">BASE</div><div style="font-size:15px; font-weight:800; color:#3d4c5f; font-variant-numeric:tabular-nums;">{{ o.base }}</div></div>
5708                    <div style="min-width:130px;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.buyerLabel }}</div><div style="font-size:13px; font-weight:700; color:#112033;">{{ o.buyer }}</div></div>
5709                    <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.amountLabel }}</div><div style="font-size:17px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div>
5710                   <span style="display:inline-flex;align-items:center;gap:4px;font-size:11px; font-weight:800; padding:6px 10px; border-radius:100px; color:{{ o.variationColor }}; background:{{ o.variationBg }};"><sc-if value="{{ o.variationUp }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-up" aria-hidden="true"></i></sc-if><sc-if value="{{ o.variationDown }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-down" aria-hidden="true"></i></sc-if>{{ o.variation }}</span>
5711                    <span style="font-size:11px; font-weight:800; padding:6px 11px; border-radius:100px; background:#eef4fb; color:#0a5ca8;">{{ o.count }}</span>
5712                    <button onClick="{{ o.open }}" style="background:#002e5f; color:#fff; border:none; border-radius:9px; padding:9px 14px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer;">Ver ofertas</button>
5713                  </div>
5714                </sc-for>
5715                <sc-if value="{{ sellerClosedPaginated }}" hint-placeholder-val="{{ false }}">
5716                  <div style="display:flex; align-items:center; justify-content:center; gap:12px; padding:14px 20px; border-top:1px solid #e8eef5;">
5717                    <button onClick="{{ sellerClosedPrev }}" disabled="{{ sellerClosedPrevDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Anterior</button>
5718                    <span style="font-size:12px; color:#65758a; font-weight:700;">{{ sellerClosedPageLabel }}</span>
5719                    <button onClick="{{ sellerClosedNext }}" disabled="{{ sellerClosedNextDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Siguiente</button>
5720                  </div>
5721                </sc-if>
5722              </details>
5723            </sc-if>
5724          </div>
5725        </sc-if>
5726
5727        <!-- MAIN ANALYTICS GRID -->
5728        <div id="profile-analytics-grid" style="display:grid; grid-template-columns:{{ dashCols }}; gap:16px; margin-bottom:16px;">
5729          <!-- activity area chart -->
5730          <div data-reveal style="background:#fff; border-radius:18px; padding:20px 22px; box-shadow:0 8px 26px rgba(0,30,60,0.07);">
5731            <div style="display:flex; justify-content:space-between; align-items:baseline; flex-wrap:wrap; gap:8px;">
5732            <div><div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">{{ activityEyebrow }}</div><h3 style="font-size:19px; font-weight:800; margin:3px 0 0; color:#112033;">{{ activityTitle }}</h3></div>
5733              <div style="text-align:right;"><div style="font-size:22px; font-weight:900; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ activityTotal }}</div><div style="font-size:11px; color:#65758a;">{{ activityUnit }}</div></div>
5734            </div>
5735            <svg viewBox="0 0 560 200" preserveAspectRatio="none" style="width:100%; height:190px; margin-top:14px; display:block; overflow:visible;">
5736              <defs><linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#0a5ca8" stop-opacity="0.28"></stop><stop offset="100%" stop-color="#0a5ca8" stop-opacity="0"></stop></linearGradient></defs>
5737              <sc-for list="{{ gridLines }}" as="g" hint-placeholder-count="4"><line x1="0" y1="0" data-dc-svg-y1="{{ g }}" x2="560" y2="0" data-dc-svg-y2="{{ g }}" stroke="#eef3f9" stroke-width="1"></line></sc-for>
5738              <path d="M0 0" data-dc-svg-d="{{ areaD }}" fill="url(#areaFill)" style="opacity:0; animation:fadeScale 1s ease forwards 0.6s;"></path>
5739              <path d="M0 0" data-dc-svg-d="{{ lineD }}" fill="none" stroke="#0a5ca8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" pathLength="1" style="stroke-dasharray:1; stroke-dashoffset:1; animation:drawLine 1.6s cubic-bezier(0.22,0.72,0.18,1) forwards 0.3s;"></path>
5740              <path d="M0 0" data-dc-svg-d="{{ lineD2 }}" fill="none" stroke="#1f7a63" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" pathLength="1" style="stroke-dasharray:1; stroke-dashoffset:1; animation:drawLine 1.6s cubic-bezier(0.22,0.72,0.18,1) forwards 0.5s;"></path>
5741            </svg>
5742            <div style="display:flex; gap:18px; margin-top:8px; font-size:12px;">
5743              <span style="display:inline-flex; align-items:center; gap:6px; color:#3d4c5f;"><span style="width:14px; height:3px; border-radius:2px; background:#0a5ca8;"></span>{{ activityPrimaryLegend }}</span>
5744              <span style="display:inline-flex; align-items:center; gap:6px; color:#3d4c5f;"><span style="width:14px; height:3px; border-radius:2px; background:#1f7a63;"></span>{{ activityMarketLegend }}</span>
5745            </div>
5746          </div>
5747          <!-- donut distribution -->
5748          <div data-reveal style="background:#fff; border-radius:18px; padding:20px 22px; box-shadow:0 8px 26px rgba(0,30,60,0.07); display:flex; flex-direction:column;">
5749            <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">{{ distributionEyebrow }}</div>
5750            <h3 style="font-size:19px; font-weight:800; margin:3px 0 14px; color:#112033;">{{ distributionTitle }}</h3>
5751            <div style="display:flex; align-items:center; justify-content:center; margin:6px 0 14px;">
5752              <div style="position:relative; width:150px; height:150px; border-radius:50%; background:{{ donutGradient }}; animation:spinIn 1s cubic-bezier(0.22,0.72,0.18,1) both 0.3s;">
5753                <div style="position:absolute; inset:22px; border-radius:50%; background:#fff; display:grid; place-items:center; text-align:center;"><div><div style="font-size:24px; font-weight:900; color:#112033; font-variant-numeric:tabular-nums;">{{ donutTotal }}</div><div style="font-size:10.5px; color:#65758a;">{{ distributionUnit }}</div></div></div>
5754              </div>
5755            </div>
5756            <div style="display:flex; flex-direction:column; gap:8px;">
5757              <sc-for list="{{ donutLegend }}" as="d" hint-placeholder-count="3">
5758                <div style="display:flex; align-items:center; gap:8px; font-size:13px;"><span style="width:10px; height:10px; border-radius:3px; background:{{ d.color }}; flex-shrink:0;"></span><span style="flex:1; color:#3d4c5f;">{{ d.label }}</span><strong style="color:#112033; font-variant-numeric:tabular-nums;">{{ d.pct }}</strong></div>
5759              </sc-for>
5760            </div>
5761          </div>
5762        </div>
5763
5764        </sc-if>
5765
5766        <sc-if value="{{ profileRosganVisible }}" hint-placeholder-val="{{ false }}">
5767          <section class="profile-company" style="border-left-color:{{ profileCompanyColor }};">
5768            <sc-if value="{{ profileCompanyLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ profileCompanyLogo }}" alt="{{ profileCompanyName }}"></sc-if>
5769            <div><span>MI CONSIGNATARIA</span><h2>{{ profileCompanyName }}</h2></div>
5770          </section>
5771        </sc-if>
5772        <sc-if value="{{ profileFeriasVisible }}" hint-placeholder-val="{{ false }}">
5773          <section class="profile-rosgan-lots profile-ferias">
5774            <header><div><h2>{{ profileFeriasTitle }}</h2><p>Ferias de hoy y próximos remates.</p></div><button type="button" onClick="{{ refreshProfileFerias }}">Actualizar</button></header>
5775            <sc-if value="{{ profileFeriasLoading }}" hint-placeholder-val="{{ false }}"><p role="status">Cargando remates feria…</p></sc-if>
5776            <sc-if value="{{ profileFeriasError }}" hint-placeholder-val="{{ false }}"><p role="alert">{{ profileFeriasError }}</p></sc-if>
5777            <sc-if value="{{ profileFeriasEmpty }}" hint-placeholder-val="{{ false }}"><p>No hay ferias de hoy ni próximas para mostrar.</p></sc-if>
5778            <div class="profile-feria-grid"><sc-for list="{{ profileFeriaRows }}" as="f" hint-placeholder-count="0"><article class="profile-feria-card">
5779              <div class="profile-feria-brand"><sc-if value="{{ f.hasLogo }}"><img data-dc-attr-src="{{ f.logo }}" alt="{{ f.company }}"></sc-if><span>{{ f.company }}</span><strong>{{ f.status }}</strong></div>
5780              <h3>{{ f.title }}</h3><p>{{ f.date }}</p><p>{{ f.location }}</p>
5781              <div class="profile-feria-actions">
5782                <div class="profile-feria-share-actions">
5783                  <sc-if value="{{ f.hasLink }}"><a class="profile-feria-action is-primary" href="{{ f.href }}" target="_blank" rel="noopener noreferrer"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true" width="16" height="16"><span>Ver transmisión</span></a></sc-if>
5784                  <button type="button" class="profile-feria-action" onClick="{{ f.downloadFlyer }}"><img src="/assets/icons/lucide/download.svg" alt="" aria-hidden="true" width="16" height="16"><span>Descargar flyer</span></button>
5785                  <sc-if value="{{ f.hasLink }}"><button type="button" class="profile-feria-action" onClick="{{ f.copyTransmission }}" title="Copiar URL de transmisión"><img src="/assets/icons/lucide/copy.svg" alt="" aria-hidden="true" width="16" height="16"><span>Copiar enlace</span></button></sc-if>
5786                  <sc-if value="{{ !f.hasLink }}"><button type="button" class="profile-feria-action" onClick="{{ f.reviewTransmission }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true" width="16" height="16"><span>Revisar transmisión</span></button></sc-if>
5787                </div>
5788                <div class="profile-feria-manage-actions">
5789                  <sc-if value="{{ f.canControlYoutube }}"><button type="button" class="profile-feria-action is-management" onClick="{{ f.controlYoutube }}">Controlar transmisión</button></sc-if>
5790                  <button type="button" class="profile-feria-action is-management" onClick="{{ f.editFeria }}"><img src="/assets/icons/lucide/pencil.svg" alt="" aria-hidden="true" width="16" height="16"><span>Editar</span></button>
5791                  <button type="button" class="profile-feria-action is-danger" onClick="{{ f.deleteFeria }}" disabled="{{ feriaDeleting }}">
5791<img src="/assets/icons/lucide/trash-2.svg" alt="" aria-hidden="true" width="16" height="16"><span>Eliminar</span></button>
5792                </div>
5793              </div>
5794              <sc-if value="{{ !f.hasLink }}"><small>{{ f.youtubeMessage }}</small></sc-if>
5795            </article></sc-for></div>
5796          </section>
5797        </sc-if>
5798        <!-- TABS: lotes / ofertas -->
5799        <sc-if value="{{ showProfileTabs }}" hint-placeholder-val="{{ true }}">
5800        <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px;">
5801          <sc-for list="{{ profileTabs }}" as="t" hint-placeholder-count="2">
5802            <span onClick="{{ t.go }}" style="padding:10px 20px; border-radius:100px; font-size:14px; font-weight:700; cursor:pointer; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:{{ t.color }}; transition:transform 160ms;" style-hover="transform:translateY(-2px);">{{ t.label }}<strong style="font-variant-numeric:tabular-nums; margin-left:6px;">{{ t.count }}</strong></span>
5803          </sc-for>
5804        </div>
5805        </sc-if>
5806        <sc-if value="{{ profileErrorMsg }}" hint-placeholder-val="{{ false }}">
5807          <div style="background:rgba(230,57,70,0.1); border:1px solid rgba(230,57,70,0.25); border-radius:14px; padding:12px 14px; color:#9f2330; font-size:13px; font-weight:700; margin-bottom:16px; line-height:1.35;">{{ profileErrorMsg }}</div>
5808        </sc-if>
5809        <sc-if value="{{ profileEmptyDebug }}" hint-placeholder-val="{{ false }}">
5810          <div style="background:#fff; border:1px solid #d7e2ef; border-radius:14px; padding:12px 14px; color:#516176; font-size:12px; font-weight:700; margin-bottom:16px; line-height:1.45;">
5811            {{ profileEmptyDebug }}
5812          </div>
5813        </sc-if>
5814
5815        <sc-if value="{{ profileShowRosganLots }}" hint-placeholder-val="{{ false }}">
5816          <section class="profile-rosgan-lots">
5817            <header><div><h2>Lotes · Próximos remates</h2><p>Tus cargas primero, luego las del equipo.</p></div><button onClick="{{ refreshProfileRosgan }}">Actualizar</button></header>
5818            <sc-if value="{{ profileRosganLoading }}" hint-placeholder-val="{{ false }}"><p role="status">Cargando lotes de tus consignatarias…</p></sc-if>
5819            <sc-if value="{{ profileRosganError }}" hint-placeholder-val="{{ false }}"><p role="alert">{{ profileRosganError }}</p></sc-if>
5820            <sc-if value="{{ profileRosganEmpty }}" hint-placeholder-val="{{ false }}"><p>{{ profileRosganEmptyText }}</p></sc-if>
5821            <div class="profile-rosgan-list">
5822              <sc-for list="{{ profileRosganRows }}" as="l" hint-placeholder-count="0">
5823                <article class="profile-rosgan-row">
5824                  <div><span class="profile-lot-cert">CERT. {{ l.cert }}</span><h3>{{ l.category }}</h3><span>{{ l.location }}</span></div>
5825                  <div><strong>{{ l.heads }}</strong><span>cabezas</span></div>
5826                  <div><strong>{{ l.remate }}</strong><span class="profile-lot-status">{{ l.status }}</span><sc-if value="{{ l.own }}" hint-placeholder-val="{{ false }}"><span class="profile-lot-own">Cargado por vos</span></sc-if></div>
5827                  <div class="profile-lot-actions"><a data-dc-attr-href="{{ l.href }}">Ver lote ↗</a><sc-if value="{{ l.canEdit }}" hint-placeholder-val="{{ false }}"><button onClick="{{ l.edit }}">Editar</button></sc-if></div>
5828                </article>
5829              </sc-for>
5830            </div>
5831          </section>
5832        </sc-if>
5833        <sc-if value="{{ profileShowLots }}" hint-placeholder-val="{{ true }}">
5834          <sc-if value="{{ profileLotsLoading }}" hint-placeholder-val="{{ false }}">
5835            <div style="background:#fff; border:1px solid #e3eaf2; border-radius:18px; padding:42px 24px; text-align:center; color:#65758a; font-size:13.5px;">
5836              <span style="display:inline-block; width:22px; height:22px; border:3px solid #dce6f0; border-top-color:#0a5ca8; border-radius:50%; animation:spin 800ms linear infinite; vertical-align:middle; margin-right:9px;"></span>
5837              Cargando tus lotes...
5838            </div>
5839          </sc-if>
5840          <sc-if value="{{ hasMyLots }}" hint-placeholder-val="{{ true }}">
5841            <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:16px;">
5842              <sc-for list="{{ myLotCards }}" as="l" hint-placeholder-count="4">
5843                <div onClick="{{ l.open }}" data-reveal style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); cursor:pointer; transition:transform 240ms, box-shadow 240ms;" style-hover="transform:translateY(-5px); box-shadow:0 24px 56px rgba(0,30,60,0.16);">
5844                  <div style="position:relative; height:130px;">
5845                    <div style="position:absolute; inset:0; background-image:url('{{ l.image }}'); background-size:cover; background-position:center;"></div>
5846                    <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 45%, rgba(0,29,61,0.72));"></div>
5847                    <span style="position:absolute; top:10px; left:10px; background:{{ l.stateBg }}; color:{{ l.stateColor }}; font-size:11px; font-weight:800; padding:5px 11px; border-radius:100px;">{{ l.state }}</span>
5848                    <div style="position:absolute; bottom:8px; left:12px; right:12px; color:#fff;"><div style="font-size:14.5px; font-weight:800; line-height:1.15;">{{ l.categoria }}</div><div style="font-size:11px; color:#c8d8ea;">Cert. {{ l.cert }} &middot; {{ l.origen }}</div></div>
5849                  </div>
5850                  <div style="padding:12px 14px;">
5851                    <div style="display:flex; justify-content:space-between; align-items:center;">
5852                      <div><div style="font-size:10.5px; color:#8593a6;">{{ l.bidTitle }}</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.bid }}</div></div>
5853                      <div style="text-align:right;"><div style="font-size:10.5px; color:#8593a6;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033;">{{ l.cabezas }}</div></div>
5854                    </div>
5855                    <sc-if value="{{ l.notExpired }}" hint-placeholder-val="{{ true }}">
5856                      <div style="margin-top:10px; height:6px; background:#eef3f9; border-radius:100px; overflow:hidden;"><div style="height:100%; width:{{ l.progress }}; border-radius:100px; background:linear-gradient(90deg,#0a5ca8,#1f7a63); transform:scaleX(0); transform-origin:left; animation:growBar 0.9s cubic-bezier(0.22,0.72,0.18,1) forwards 0.2s;"></div></div>
5857                      <div style="font-size:11px; color:#65758a; margin-top:5px;">{{ l.progressLabel }}</div>
5858                      <sc-if value="{{ l.canEdit }}" hint-placeholder-val="{{ false }}">
5859                        <button onClick="{{ l.edit }}" style="margin-top:10px; width:100%; display:inline-flex; align-items:center; justify-content:center; gap:8px; background:#fff; color:#002e5f; border:1px solid #cbd9e8; border-radius:10px; padding:10px; font-size:13px; font-weight:800; font-family:inherit; cursor:pointer; transition:all 160ms;" style-hover="background:#eef5fb; border-color:#0a5ca8; transform:translateY(-1px);" style-active="transform:scale(0.98);"><img src="assets/icons/lucide/pencil.svg" alt="" style="width:15px; height:15px;">Editar lote</button>
5860                      </sc-if>
5861                    </sc-if>
5862                    <sc-if value="{{ l.expired }}" hint-placeholder-val="{{ false }}">
5863                      <div style="font-size:11.5px; color:#8593a6; margin-top:9px;">La subasta caducó. Podés programar una nueva fecha.</div>
5864                      <sc-if value="{{ l.canRepublish }}" hint-placeholder-val="{{ false }}">
5865                        <button onClick="{{ l.republish }}" style="margin-top:10px; width:100%; display:inline-flex; align-items:center; justify-content:center; gap:8px; background:#002e5f; color:#fff; border:none; border-radius:10px; padding:11px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.97);"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true"></i> Republicar</button>
5866                      </sc-if>
5867                    </sc-if>
5868                  </div>
5869                </div>
5870              </sc-for>
5871            </div>
5872          </sc-if>
5873          <sc-if value="{{ myLotsEmpty }}" hint-placeholder-val="{{ false }}">
5874            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
5875              <div style="font-size:16px; font-weight:700; color:#112033;">Todavía no cargaste lotes</div>
5876              <div style="font-size:13.5px; margin-top:4px;">Publicá tu primer lote y aparecerá acá con su rendimiento.</div>
5877              <button onClick="{{ goLoad }}" style="margin-top:16px; background:#e63946; color:#fff; border:none; border-radius:10px; padding:11px 22px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Cargar lote</button>
5878            </div>
5879          </sc-if>
5880        </sc-if>
5881
5882        <sc-if value="{{ profileShowOffers }}" hint-placeholder-val="{{ false }}">
5883          <sc-if value="{{ myBidsLoading }}" hint-placeholder-val="{{ false }}">
5884            <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando tus ofertas…</div>
5885          </sc-if>
5886          <sc-if value="{{ hasOffers }}" hint-placeholder-val="{{ false }}">
5887            <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden;">
5888              <sc-for list="{{ offerRows }}" as="o" hint-placeholder-count="4">
5889                <div onClick="{{ o.open }}" data-reveal style="display:flex; align-items:center; gap:14px; padding:14px 18px; border-bottom:1px solid #eef3f9; cursor:pointer; transition:background 160ms;" style-hover="background:#f6f9fc;">
5890                  <div style="width:56px; height:56px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0;"></div>
5891                  <div style="flex:1; min-width:0;"><div style="font-size:15px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12.5px; color:#65758a;">Cert. {{ o.cert }} &middot; {{ o.origen }}</div></div>
5892                  <div style="text-align:right;"><div style="font-size:10.5px; color:#8593a6;">MI OFERTA</div><div style="font-size:16px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div>
5893                  <sc-if value="{{ o.standing }}" hint-placeholder-val="{{ false }}"><span style="font-size:11px; font-weight:800; padding:5px 11px; border-radius:100px; background:{{ o.standBg }}; color:{{ o.standColor }}; animation:badgePop 340ms cubic-bezier(0.34,1.56,0.64,1) both;">{{ o.standLabel }}</span></sc-if>
5894                  <span style="font-size:11px; font-weight:800; padding:5px 11px; border-radius:100px; background:{{ o.statusBg }}; color:{{ o.statusColor }};">{{ o.status }}</span>
5895                </div>
5896              </sc-for>
5897            </div>
5898          </sc-if>
5899          <sc-if value="{{ offersEmpty }}" hint-placeholder-val="{{ false }}">
5900            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
5901              <div style="font-size:16px; font-weight:700; color:#112033;">Todavía no hiciste ofertas</div>
5902              <div style="font-size:13.5px; margin-top:4px;">Explorá el pool de lotes y ofertá por el que te interese.</div>
5903              <button onClick="{{ goPool }}" style="margin-top:16px; background:#e63946; color:#fff; border:none; border-radius:10px; padding:11px 22px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Ver pool de lotes</button>
5904            </div>
5905          </sc-if>
5906        </sc-if>
5907
5908        <sc-if value="{{ profileShowPending }}" hint-placeholder-val="{{ false }}">
5909          <sc-if value="{{ pendingLoading }}" hint-placeholder-val="{{ false }}">
5910            <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando ofertas por aprobar…</div>
5911          </sc-if>
5912          <sc-if value="{{ hasPending }}" hint-placeholder-val="{{ false }}">
5913            <div style="display:flex; flex-direction:column; gap:12px;">
5914              <sc-for list="{{ pendingRows }}" as="p" hint-placeholder-count="3">
5915                <div data-reveal style="background:#fff; border-radius:16px; box-shadow:0 8px 26px rgba(0,30,60,0.07); padding:14px 18px; display:flex; align-items:center; gap:14px; flex-wrap:wrap;">
5916                  <div onClick="{{ p.open }}" style="width:56px; height:56px; border-radius:12px; background-image:url('{{ p.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer;"></div>
5917                  <div onClick="{{ p.open }}" style="flex:1; min-width:140px; cursor:pointer;"><div style="font-size:15px; font-weight:700; color:#112033;">{{ p.categoria }}</div><div style="font-size:12.5px; color:#65758a;">Cert. {{ p.cert }} &middot; {{ p.bidder }}</div></div>
5918                  <div style="text-align:right;"><div style="font-size:10.5px; color:#8593a6;">OFERTA</div><div style="font-size:17px; font-weight:800; color:#a8730a; font-variant-numeric:tabular-nums;">{{ p.amount }}</div></div>
5919                  <div style="display:flex; gap:8px;">
5920                    <button onClick="{{ p.reject }}" disabled="{{ p.disabled }}" style="background:#fff; color:#c22; border:1px solid #f2c4c8; border-radius:10px; padding:10px 16px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#fdeef0;">Rechazar</button>
5921                    <button onClick="{{ p.approve }}" disabled="{{ p.disabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#186552;">{{ p.approveLabel }}</button>
5922                  </div>
5923                </div>
5924              </sc-for>
5925            </div>
5926          </sc-if>
5927          <sc-if value="{{ pendingEmpty }}" hint-placeholder-val="{{ false }}">
5928            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
5929              <div style="font-size:16px; font-weight:700; color:#112033;">No hay ofertas pendientes</div>
5930              <div style="font-size:13.5px; margin-top:4px;">Las ofertas de tus compradores aparecerán acá para aprobar o rechazar.</div>
5931            </div>
5932          </sc-if>
5933        </sc-if>
5934
5935        <sc-if value="{{ profileShowComitentes }}" hint-placeholder-val="{{ false }}">
5936          <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px;">
5937             <span style="font-size:13.5px; color:#65758a;">Comitentes de tus consignatarias.</span>
5938             <sc-if value="{{ profileCanEditRelations }}" hint-placeholder-val="false"><button type="button" onClick="{{ openProfileComitenteMerge }}" style="background:#eef5fc;color:#073d70;border:1px solid #cadbea;border-radius:10px;padding:10px 18px;font:inherit;cursor:pointer;">Fusionar comitentes</button><button onClick="{{ toggleComitenteForm }}" style="background:#e63946; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nuevo comitente</button></sc-if>
5939          </div>
5940          <sc-if value="{{ comitenteFormOpen }}" hint-placeholder-val="{{ false }}">
5941            <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.08); padding:20px 22px; margin-bottom:16px;">
5942              <div style="font-size:15px; font-weight:800; color:#112033; margin-bottom:14px;">Alta de comitente</div>
5943              <div class="establishment-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
5944                <label style="display:flex; flex-direction:column; gap:6px;"><span style="display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px; font-weight:600; color:#3d4c5f;"><span>CUIT (opcional)</span><small style="color:#0a5ca8;">{{ cfLookupStatus }}</small></span><input aria-describedby="confidential-cf_cuitCuil" value="" data-dc-attr-value="{{ cf_cuitCuil }}" onInput="{{ set_cf_cuitCuil }}" inputmode="numeric" maxlength="11" placeholder="11 dígitos" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"><small id="confidential-cf_cuitCuil" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">&#9888;</span><span>Dato confidencial perteneciente a la firma consignataria</span></small></label>
5945                <label style="display:grid;gap:6px;color:#60758c;font-size:12px;font-weight:700;">Alias (opcional)<input type="text" value="" data-dc-attr-value="{{ cf_alias }}" onInput="{{ set_cf_alias }}" placeholder="Ej.: Estancia El Ombú" style="box-sizing:border-box;min-width:0;width:100%;height:43px;border:1px solid #ccdae8;border-radius:11px;padding:0 12px;font:inherit;background:#fff;"></label>
5946<label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Razón social</span><input value="" data-dc-attr-value="{{ cf_nombreFiscal }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f7;"></label>
5947                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Provincia fiscal</span><input value="" data-dc-attr-value="{{ cf_provincia }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f7;"></label>
5948                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Localidad fiscal</span><input value="" data-dc-attr-value="{{ cf_localidad }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f7;"></label>
5949              </div>
5950              <sc-if value="{{ comitenteFormMsg }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ comitenteFormMsgBg }}; color:{{ comitenteFormMsgColor }};">{{ comitenteFormMsg }}</div></sc-if>
5951              <div style="display:flex; gap:10px; margin-top:16px;">
5951<button onClick="{{ comitenteSave }}" disabled="{{ comitenteSaveDisabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">{{ comitenteSaveLabel }}</button><button onClick="{{ toggleComitenteForm }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button></div>
5952            </div>
5953          </sc-if>
5954          <sc-if value="{{ comitentesLoading }}" hint-placeholder-val="{{ false }}"><div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando comitentes…</div></sc-if>
5955          <div class="pc-directory-tools">
5956            <label class="pc-search"><span>Buscar comitente</span><input type="search" value="" data-dc-attr-value="{{ pcQuery }}" onInput="{{ pcSearch }}" placeholder="Alias, nombre fiscal o CUIT"></label>
5957            <label><span>Provincia</span><select value="" aria-label="Filtrar por provincia" data-dc-attr-value="{{ pcProvince }}" onChange="{{ pcSetProvince }}"><option value="">Todas las provincias</option><sc-for list="{{ pcProvinces }}" as="p"><option value="" data-dc-attr-value="{{ p.value }}" label="{{ p.value }}">{{ p.value }}</option></sc-for></select></label>
5958            <label><span>Localidad</span><select value="" aria-label="Filtrar por localidad" data-dc-attr-value="{{ pcLocality }}" onChange="{{ pcSetLocality }}"><option value="">Todas las localidades</option><sc-for list="{{ pcLocalities }}" as="l"><option value="" data-dc-attr-value="{{ l.value }}" label="{{ l.value }}">{{ l.value }}</option></sc-for></select></label>
5959          </div>
5960          <div class="pc-directory-summary"><span role="status">{{ pcCount }}</span><sc-if value="{{ pcHasFilters }}"><button type="button" onClick="{{ pcClear }}">Limpiar filtros</button></sc-if></div>
5961          <sc-if value="{{ comitentesHas }}"><ul class="pc-directory-list" aria-label="Comitentes"><sc-for list="{{ comitenteRows }}" as="c"><li class="pc-directory-row"><div class="pc-directory-name"><strong>{{ c.razonSocial }}</strong><span>{{ c.fiscalName }}</span></div><div class="pc-directory-cuit"><small>CUIT</small><span>{{ c.cuitCuil }}</span></div><div class="pc-directory-location"><small>Ubicación</small><span>{{ c.meta }}</span></div><sc-if value="{{ profileCanEditRelations }}"><button type="button" onClick="{{ c.editAlias }}" aria-label="Editar alias de {{ c.razonSocial }}">Editar alias</button></sc-if></li></sc-for></ul></sc-if>
5962          <sc-if value="{{ comitentesEmpty }}"><div class="pc-directory-empty">{{ pcEmptyLabel }}</div></sc-if>
5963          <sc-if value="{{ pcPaginated }}"><nav class="pc-directory-pages" aria-label="Páginas de comitentes"><button type="button" onClick="{{ pcPrev }}" disabled="{{ pcPrevDisabled }}">Anterior</button><span>{{ pcPageLabel }}</span><button type="button" onClick="{{ pcNext }}" disabled="{{ pcNextDisabled }}">Siguiente</button></nav></sc-if>
5964        </sc-if>
5965
5966        <sc-if value="{{ profileShowEst }}" hint-placeholder-val="{{ false }}">
5967          <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px;">
5968            <span style="font-size:13.5px; color:#65758a;">Establecimientos guardados para reutilizar al cargar lotes.</span>
5969            <sc-if value="{{ canCreateEstablishment }}" hint-placeholder-val="{{ false }}"><button onClick="{{ toggleEstForm }}" style="background:#e63946; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nuevo establecimiento</button></sc-if>
5970          </div>
5971          <sc-if value="{{ estFormOpen }}" hint-placeholder-val="{{ false }}">
5972            <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.08); padding:20px 22px; margin-bottom:16px;">
5973              <div style="font-size:15px; font-weight:800; color:#112033; margin-bottom:14px;">Nuevo establecimiento</div>
5974              <div class="establishment-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
5975                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Comitente *</span><select value="" data-dc-attr-value="{{ ef_comitenteId }}" onChange="{{ set_ef_comitenteId }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
5975 color:#112033; background:#fff;"><option value="">Seleccionar comitente…</option><sc-for list="{{ efComitenteOpts }}" as="c" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ c.id }}">{{ c.label }}</option></sc-for></select></label>
5976                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Nombre del establecimiento *</span><input value="" data-dc-attr-value="{{ ef_nombre }}" onInput="{{ set_ef_nombre }}" placeholder="Ej: Estancia El Ceibo" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
5977                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Provincia *</span><select value="" data-dc-attr-value="{{ ef_provincia }}" onChange="{{ set_ef_provincia }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">{{ efProvPlaceholder }}</option><sc-for list="{{ efProvinceOpts }}" as="p" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for></select></label>
5978                <label style="position:relative;display:flex; flex-direction:column; gap:6px;"><span style="display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px; font-weight:600; color:#3d4c5f;"><span>Localidad *</span><small style="display:inline-flex;align-items:center;gap:4px;color:#718296;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;object-fit:contain;">Google</small></span><input value="" data-dc-attr-value="{{ ef_localidad }}" onInput="{{ set_ef_localidad }}" onFocus="{{ openEfPlaces }}" onBlur="{{ closeEfPlaces }}" disabled="{{ efLocDisabled }}" autocomplete="off" placeholder="Escribí una localidad" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 38px 11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:{{ efLocBg }};"><sc-if value="{{ efPlaceLoading }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:13px;top:39px;color:#0a5ca8;"></i></sc-if><sc-if value="{{ efPlaceResultsOpen }}" hint-placeholder-val="{{ false }}"><div style="position:absolute;z-index:50;left:0;right:0;top:calc(100% + 5px);padding:5px;border:1px solid #d6e2ed;border-radius:11px;background:#fff;box-shadow:0 14px 30px rgba(0,35,70,.18);max-height:220px;overflow:auto;"><sc-for list="{{ efPlaceResults }}" as="p" hint-placeholder-count="4"><button type="button" onClick="{{ p.pick }}" style="width:100%;display:grid;gap:2px;border:0;border-radius:8px;background:#fff;padding:8px 9px;text-align:left;cursor:pointer;"><strong style="font-size:11.5px;color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px;color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if></label>
5979                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">RENSPA <small style="font-weight:500; color:#8593a6;">opcional</small></span><input value="" data-dc-attr-value="{{ ef_renspa }}" onInput="{{ set_ef_renspa }}" placeholder="Podés dejarlo vacío" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label>
5980              </div>
5981              <sc-if value="{{ estFormMsg }}" hint-placeholder-val="{{ false }}">
5982                <div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ estFormMsgBg }}; color:{{ estFormMsgColor }};">{{ estFormMsg }}</div>
5983              </sc-if>
5984              <div style="display:flex; gap:10px; margin-top:16px;">
5985                <button onClick="{{ saveEst }}" disabled="{{ estSaveDisabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:{{ estSaveCursor }}; opacity:{{ estSaveOpacity }};">{{ estSaveLabel }}</button>
5986                <button onClick="{{ toggleEstForm }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button>
5987              </div>
5988            </div>
5989          </sc-if>
5990          <sc-if value="{{ estLoading }}" hint-placeholder-val="{{ false }}">
5991            <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando establecimientos…</div>
5992          </sc-if>
5993          <sc-if value="{{ estHas }}" hint-placeholder-val="{{ false }}">
5994            <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:16px;">
5995              <sc-for list="{{ estRowsList }}" as="e" hint-placeholder-count="3">
5996                <div data-reveal style="background:#fff; border-radius:16px; box-shadow:0 8px 26px rgba(0,30,60,0.07); padding:18px 20px; display:flex; flex-direction:column; gap:6px;">
5997                  <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
5998                    <div style="min-width:0;"><div style="font-size:16px; font-weight:800; color:#112033;">{{ e.nombre }}</div><div style="font-size:12.5px; color:#65758a;">{{ e.meta }}</div></div>
5999                     <sc-if value="{{ profileCanEditRelations }}" hint-placeholder-val="false"><button onClick="{{ e.remove }}" disabled="{{ e.disabled }}" title="Eliminar" style="flex-shrink:0; background:#fff; color:#c22; border:1px solid #f2c4c8; border-radius:9px; padding:7px 11px; font-size:12px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#fdeef0;">Eliminar</button></sc-if>
6000                  </div>
6001                  <sc-if value="{{ e.comitente }}" hint-placeholder-val="{{ false }}"><div style="font-size:12.5px; color:#3d4c5f;">Comitente: <strong>{{ e.comitente }}</strong></div></sc-if>
6002                  <sc-if value="{{ e.comitenteCuitCuil }}" hint-placeholder-val="{{ false }}"><div style="font-size:11.5px; color:#65758a;">CUIT/CUIL {{ e.comitenteCuitCuil }}</div></sc-if>
6003                  <sc-if value="{{ e.renspa }}" hint-placeholder-val="{{ false }}"><div style="font-size:11.5px; color:#65758a;">RENSPA {{ e.renspa }}</div></sc-if>
6004                  <sc-if value="{{ e.hasCoords }}" hint-placeholder-val="{{ false }}"><div style="font-size:11.5px; color:#8593a6; font-variant-numeric:tabular-nums;"><i class="dc-lucide dc-lucide-map-pin" aria-hidden="true"></i> {{ e.coords }}</div></sc-if>
6005                </div>
6006              </sc-for>
6007            </div>
6008          </sc-if>
6009          <sc-if value="{{ estEmpty }}" hint-placeholder-val="{{ false }}">
6010            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
6011              <div style="font-size:16px; font-weight:700; color:#112033;">Todavía no tenés establecimientos</div>
6012              <div style="font-size:13.5px; margin-top:4px;">Cargá uno para reutilizarlo al publicar lotes.</div>
6013            </div>
6014          </sc-if>
6015        </sc-if>
6016
6017        <!-- PANEL: COMPRADORES (autorizaciones) -->
6018        <sc-if value="{{ profileShowBuyers }}" hint-placeholder-val="{{ false }}">
6019          <section id="seller-buyers-panel" style="background:#f8fafc; border:1px solid #e3eaf2; border-radius:18px; padding:20px; margin-bottom:16px;">
6020          <div style="display:flex; align-items:end; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:14px;">
6021            <div>
6022              <div style="font-size:11px; letter-spacing:0.12em; color:#0a5ca8; font-weight:800; text-transform:uppercase;">Autorizaci&oacute;
6022n y cr&eacute;ditos</div>
6023              <h3 style="font-size:20px; color:#112033; margin:3px 0 0;">Cuentas comprador</h3>
6024            </div>
6025          </div>
6026          <label style="display:flex; align-items:center; gap:10px; background:#fff; border:1px solid #dbe5ef; border-radius:12px; padding:10px 13px; margin-bottom:12px;">
6027            <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#65758a" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-4-4"></path></svg>
6028            <input value="" data-dc-attr-value="{{ buyerDirectorySearch }}" onInput="{{ setBuyerDirectorySearch }}" placeholder="Buscar comprador por nombre o email" style="border:none; outline:none; width:100%; background:transparent; color:#112033; font:inherit; font-size:13.5px;">
6029          </label>
6030          <sc-if value="{{ buyerDirectoryLoading }}" hint-placeholder-val="{{ false }}">
6031            <div style="background:#fff; border:1px solid #eef3f9; border-radius:16px; padding:34px; text-align:center; color:#65758a; font-size:13.5px;">Cargando cuentas comprador...</div>
6032          </sc-if>
6033          <sc-if value="{{ buyerDirectoryHas }}" hint-placeholder-val="{{ false }}">
6034            <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:12px; margin-bottom:26px;">
6035              <sc-for list="{{ buyerDirectoryRows }}" as="u" hint-placeholder-count="4">
6036                <div style="background:#fff; border:1px solid #e3eaf2; border-radius:14px; padding:14px 15px; display:flex; align-items:center; gap:11px; min-width:0;">
6037                  <span style="width:40px; height:40px; border-radius:50%; background:#e7f1fb; color:#003d73; display:grid; place-items:center; font-size:13px; font-weight:800; flex-shrink:0;">{{ u.initials }}</span>
6038                  <div style="flex:1; min-width:0;">
6039                    <div style="font-size:14px; font-weight:800; color:#112033; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.name }}</div>
6040                    <div style="font-size:11.5px; color:#65758a; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.email }}</div>
6041                    <div style="font-size:10.5px; color:#8a98aa; margin-top:3px;">{{ u.lastAccess }}</div>
6042                  </div>
6043                  <button onClick="{{ u.authorize }}" title="Autorizar comprador" style="background:#eef4fb; color:#003d73; border:1px solid #d5e2ef; border-radius:9px; padding:8px 10px; font-size:11.5px; font-weight:800; font-family:inherit; cursor:pointer; flex-shrink:0;">Autorizar</button>
6044                </div>
6045              </sc-for>
6046            </div>
6047          </sc-if>
6048          <sc-if value="{{ buyerDirectoryEmpty }}" hint-placeholder-val="{{ false }}">
6049            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:16px; padding:34px 20px; text-align:center; color:#65758a; margin-bottom:26px;">{{ buyerDirectoryEmptyText }}</div>
6050          </sc-if>
6051          <div style="border-top:1px solid #dfe7f0; padding-top:22px; margin-top:4px;">
6052          <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px;">
6053            <div><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;"><div style="font-size:11px; letter-spacing:0.12em; color:#1f7a63; font-weight:800; text-transform:uppercase;">Créditos semanales vigentes</div><span class="credit-live-status">EN TIEMPO REAL</span></div><span style="font-size:13.5px; color:#65758a; max-width:620px;">Seleccioná un comprador, definí el inicio y el monto. El crédito vence a los 7 días corridos, incluyendo la fecha inicial.</span></div>
6054            <button type="button" onClick="{{ sellerCreditHistory }}" style="min-height:40px; display:inline-flex; align-items:center; gap:7px; border:1px solid #bfd3e7; border-radius:10px; background:#fff; color:#075ca8; padding:8px 13px; font:800 11.5px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true"></i> Historial</button>
6055          </div>
6056          <sc-if value="{{ baFormOpen }}" hint-placeholder-val="{{ false }}">
6057            <div id="buyer-auth-form" style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.08); padding:20px 22px; margin-bottom:16px; animation:fadeScale 260ms ease both;">
6058              <div style="font-size:15px; font-weight:800; color:#112033; margin-bottom:14px;">Nuevo crédito semanal</div>
6059              <div style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
6060                <div style="background:#f4f8fc; border:1px solid #dde7f2; border-radius:12px; padding:11px 13px;"><div style="font-size:10.5px; color:#8593a6; font-weight:800; text-transform:uppercase;">Comprador seleccionado</div><strong style="display:block; margin-top:4px; font-size:14px; color:#112033;">{{ ba_selectedBuyer }}</strong><span style="display:block; margin-top:2px; font-size:11.5px; color:#65758a;">Autorización por crédito semanal</span></div>
6061                <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
6062                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Inicio del crédito</span><input value="" data-dc-attr-value="{{ ba_creditStartDate }}" onInput="{{ set_ba_creditStartDate }}" type="date" required style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
6062 color:#112033;" style-focus="border-color:#0a5ca8;"></label>
6063                  <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Crédito semanal</span><input value="" data-dc-attr-value="{{ ba_weeklyCreditAmount }}" onInput="{{ set_ba_weeklyCreditAmount }}" type="text" inputmode="numeric" required placeholder="Ej. 50.000.000" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label>
6064                </div>
6065              </div>
6066              <sc-if value="{{ baFormMsg }}" hint-placeholder-val="{{ false }}">
6067                <div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ baFormMsgBg }}; color:{{ baFormMsgColor }};">{{ baFormMsg }}</div>
6068              </sc-if>
6069              <div style="display:flex; gap:10px; margin-top:16px;">
6070                <button onClick="{{ saveBuyerAuth }}" disabled="{{ baSaving }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#186552;">{{ baSaveLabel }}</button>
6071                <button onClick="{{ toggleBaForm }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button>
6072              </div>
6073            </div>
6074          </sc-if>
6075          <sc-if value="{{ baLoading }}" hint-placeholder-val="{{ false }}">
6076            <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando compradores…</div>
6077          </sc-if>
6078          <sc-if value="{{ baHas }}" hint-placeholder-val="{{ false }}">
6079            <div class="ba-admin-grid">
6080              <sc-for list="{{ baRows }}" as="b" hint-placeholder-count="3">
6081                <div class="ba-admin-card" data-reveal style="opacity:{{ b.cardOpacity }}; transition:opacity 260ms;">
6082                  <div style="display:flex; align-items:center; gap:12px;">
6083                    <span style="width:42px; height:42px; border-radius:12px; background:{{ b.avatarBg }}; color:#fff; display:grid; place-items:center; font-size:14px; font-weight:800; flex-shrink:0; transition:background 260ms;">{{ b.initials }}</span>
6084                    <div style="flex:1; min-width:0;">
6085                      <div style="font-size:14.5px; font-weight:800; color:#112033; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ b.buyer }}</div>
6086                      <sc-if value="{{ b.hasBuyerDetail }}" hint-placeholder-val="{{ false }}"><div style="font-size:11px; color:#65758a; margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ b.buyerDetail }}</div></sc-if>
6087                      <div style="font-size:11.5px; color:#8593a6;">{{ b.validityLabel }}</div>
6088                    </div>
6089                    <span style="font-size:10.5px; font-weight:800; padding:4px 10px; border-radius:100px; background:{{ b.statusBg }}; color:{{ b.statusColor }}; white-space:nowrap; transition:all 200ms;">{{ b.statusLabel }}</span>
6090                  </div>
6091                  <div class="ba-admin-editor">
6092                    <label class="ba-admin-field"><span>Inicio del crédito</span><input value="" data-dc-attr-value="{{ b.creditStartDate }}" onInput="{{ b.setStartDate }}" type="date"></label>
6093                    <label class="ba-admin-field"><span>Crédito semanal</span><input value="" data-dc-attr-value="{{ b.weeklyCreditAmount }}" onInput="{{ b.setWeeklyCreditAmount }}" type="text" inputmode="numeric"></label>
6094                    <button class="ba-admin-save" onClick="{{ b.save }}" disabled="{{ b.busy }}">{{ b.saveLabel }}</button>
6095                  </div>
6096                  <sc-if value="{{ b.inlineLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; gap:8px; margin-top:12px; padding:12px; border-radius:11px; background:#f7fafc; color:#65758a; font-size:11px; font-weight:700;"><span style="width:15px; height:15px; border:2px solid #d8e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>Cargando uso vigente...</div></sc-if>
6097                  <sc-if value="{{ b.inlineHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; padding:11px 12px; border:1px solid #f0c7cc; border-radius:11px; background:#fff5f6; color:#9d2837; font-size:11px;">No se pudo cargar el uso vigente.<button type="button" onClick="{{ b.refreshInline }}" style="margin-left:8px; border:0; background:transparent; color:#075ca8; font:800 10px inherit; cursor:pointer;">Reintentar</button></div></sc-if>
6098                  <sc-if value="{{ b.inlineReady }}" hint-placeholder-val="{{ false }}">
6099                    <div style="margin-top:12px; padding:12px; border:1px solid #dfe9f3; border-radius:12px; background:#f8fbfd;">
6100                      <div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px;"><div><span style="display:block; color:#718397; font-size:8px; font-weight:900;">AUTORIZADO</span><strong style="display:block; margin-top:3px; color:#12314f; font-size:12px;">{{ b.inlineAuthorized }}</strong></div><div><span style="display:block; color:#718397; font-size:8px; font-weight:900;">USADO</span><strong style="display:block; margin-top:3px; color:#9b6513; font-size:12px;">{{ b.inlineUsed }}</strong></div><div><span style="display:block; color:#718397; font-size:8px; font-weight:900;">DISPONIBLE</span><strong style="display:block; margin-top:3px; color:#1f7a63; font-size:12px;">{{ b.inlineAvailable }}</strong></div></div>
6101                      <div style="display:flex; justify-content:space-between; gap:8px; margin:10px 0 5px; color:#65758a; font-size:9px; font-weight:850;"><span>USO VIGENTE</span><span>{{ b.inlineProgressLabel }}</span></div><div style="height:7px; border-radius:999px; background:#dce7f1; overflow:hidden;"><div style="width:{{ b.inlineProgress }}; height:100%; border-radius:inherit; background:linear-gradient(90deg,#0a5ca8,#1f9b7a); transition:width 500ms ease;"></div></div>
6102                      <div style="margin-top:11px; color:#0a5ca8; font-size:8.5px; letter-spacing:.08em; font-weight:900;">MOVIMIENTOS VIGENTES</div>
6103                      <sc-if value="{{ b.inlineHasMovements }}" hint-placeholder-val="{{ false }}"><div style="display:flex; flex-direction:column; gap:6px; margin-top:7px;"><sc-for list="{{ b.inlineMovements }}" as="movement" hint-placeholder-count="2"><article style="display:flex; align-items:center; justify-content:space-between; gap:7px; flex-wrap:wrap; padding:8px 9px; border:1px solid #e2eaf2; border-radius:9px; background:#fff;"><div style="min-width:105px; flex:1;"><strong style="display:block; color:#163651; font-size:10.5px;">{{ movement.lot }}</strong><span style="display:block; margin-top:2px; color:#7a8c9e; font-size:8.5px;">{{ movement.date }}</span></div><span style="padding:3px 6px; border-radius:999px; background:{{ movement.statusBg }}; color:{{ movement.statusColor }}; font-size:8px; font-weight:900;">{{ movement.statusLabel }}</span><strong style="color:#075ca8; font-size:10.5px;">{{ movement.consumedAmount }}</strong></article></sc-for></div></sc-if>
6104                      <sc-if value="{{ b.inlineEmpty }}" hint-placeholder-val="{{ false }}"><div style="margin-top:7px; color:#718397; font-size:10px;">Todavía no hay movimientos en este crédito.</div></sc-if>
6105                    </div>
6106                  </sc-if>
6107                  <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; border-top:1px solid #f0f4f9; padding-top:12px;">
6108                    <div onClick="{{ b.toggle }}" style="display:flex; align-items:center; gap:9px; cursor:pointer; user-select:none;">
6109                      <span style="position:relative; width:40px; height:22px; border-radius:100px; background:{{ b.switchBg }}; transition:background 240ms; flex-shrink:0;">
6110                        <span style="position:absolute; top:2px; left:0; transform:translateX({{ b.knobX }}); width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 2px 5px rgba(0,0,0,0.25); transition:transform 240ms cubic-bezier(0.34,1.56,0.64,1);"></span>
6111                      </span>
6112                      <span style="font-size:12.5px; font-weight:700; color:{{ b.statusColor }};">{{ b.statusLabel }}</span>
6113                    </div>
6114                    <div style="display:flex; align-items:center; gap:7px;"><button type="button" onClick="{{ b.openHistory }}" disabled="{{ b.summaryDisabled }}" style="background:#eef5fb; color:#075ca8; border:1px solid #cadeee; border-radius:9px; padding:8px 11px; font-size:11.5px; font-weight:800; font-family:inherit; cursor:pointer; opacity:{{ b.summaryOpacity }};">Historial anterior</button><button onClick="{{ b.remove }}" disabled="{{ b.busy }}" title="Eliminar autorización" style="background:#fff; color:#c22; border:1px solid #f2c4c8; border-radius:9px; padding:8px 13px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#fdeef0;">Eliminar</button></div>
6115                  </div>
6116                </div>
6117              </sc-for>
6118            </div>
6119          </sc-if>
6120          <sc-if value="{{ baEmpty }}" hint-placeholder-val="{{ false }}">
6121            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
6122              <div style="font-size:16px; font-weight:700; color:#112033;">No hay créditos vigentes</div>
6123              <div style="font-size:13.5px; margin-top:4px;">Podés crear uno nuevo o consultar los anteriores desde Historial.</div>
6124            </div>
6125          </sc-if>
6126          </div>
6127          </section>
6128        </sc-if>
6129
6130        <sc-if value="{{ launchAdminVisible }}" hint-placeholder-val="{{ false }}">
6131          <section style="margin-top:18px;background:#fff;border:1px solid #dfe8f1;border-radius:18px;box-shadow:0 10px 30px rgba(0,30,60,.06);overflow:hidden;">
6132            <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;padding:16px 18px;border-bottom:1px solid #e7eef5;background:#f8fbfe;">
6133              <div><div style="font-size:10px;letter-spacing:.12em;color:#0a5ca8;font-weight:900;">LANZAMIENTO ROSGAN</div><h3 style="margin:3px 0 0;color:#112033;font-size:17px;">Usuarios que quieren recibir el aviso</h3><span style="display:block;margin-top:3px;color:#65758a;font-size:11.5px;">{{ launchAdminCountLabel }}</span></div>
6134              <button type="button" onClick="{{ exportLaunchInterests }}" disabled="{{ launchAdminExportDisabled }}" style="height:38px;padding:0 14px;border:1px solid #bfd2e3;border-radius:9px;background:#fff;color:#075ca8;font:800 11.5px inherit;cursor:pointer;opacity:{{ launchAdminExportOpacity }};">Exportar CSV</button>
6135            </div>
6136            <div style="padding:14px 18px 18px;">
6137              <sc-if value="{{ launchAdminLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:24px;text-align:center;color:#65758a;font-size:12.5px;">Cargando interesados…</div></sc-if>
6138              <sc-if value="{{ launchAdminHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:11px 13px;border-radius:9px;background:#fff0f1;color:#b42335;font-size:12px;">{{ launchAdminError }}</div></sc-if>
6139              <sc-if value="{{ launchAdminHasRows }}" hint-placeholder-val="{{ false }}">
6140                <div class="launch-admin-list"><sc-for list="{{ launchAdminRows }}" as="interest" hint-placeholder-count="4">
6141                  <div class="launch-admin-row"><div><strong style="display:block;">{{ interest.name }}</strong><span style="display:block;margin-top:2px;font-size:10px;">{{ interest.date }}</span></div><span class="launch-admin-email">{{ interest.email }}</span><span class="launch-admin-phone">{{ interest.phone }}</span><div class="launch-admin-channels"><sc-if value="{{ interest.emailOptIn }}" hint-placeholder-val="{{ true }}"><small class="launch-admin-channel">EMAIL</small></sc-if><sc-if value="{{ interest.whatsappOptIn }}" hint-placeholder-val="{{ true }}"><small class="launch-admin-channel">WHATSAPP</small></sc-if></div></div>
6142                </sc-for></div>
6143              </sc-if>
6144              <sc-if value="{{ launchAdminEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:24px;text-align:center;color:#65758a;font-size:12.5px;">Todavía nadie pidió recibir el aviso de lanzamiento.</div></sc-if>
6145            </div>
6146          </section>
6147        </sc-if>
6148
6149        <sc-if value="{{ republishOpen }}" hint-placeholder-val="{{ false }}">
6150          <div onClick="{{ closeRepublish }}" style="position:fixed; inset:0; z-index:60; background:rgba(3,18,38,0.55); backdrop-filter:blur(3px); display:flex; align-items:center; justify-content:center; padding:20px; animation:fadeScale 220ms ease both;">
6151            <div onClick="{{ stopProp }}" style="background:#fff; border-radius:20px; box-shadow:0 30px 80px rgba(0,20,50,0.35); width:100%; max-width:460px; overflow:hidden; animation:badgePop 300ms cubic-bezier(0.34,1.56,0.64,1) both;">
6152              <div style="padding:20px 22px; border-bottom:1px solid #f0f4f9; display:flex; align-items:flex-start; justify-content:space-between; gap:12px;">
6153                <div><div style="font-size:11px; letter-spacing:0.12em; color:#e63946; font-weight:700;">REPUBLICAR LOTE</div><div style="font-size:16px; font-weight:800; color:#112033; margin-top:3px;">{{ republishTitle }}</div></div>
6154                <button onClick="{{ closeRepublish }}" title="Cerrar" style="width:32px; height:32px; border-radius:9px; background:#f6f9fc; border:1px solid #e6edf5; color:#65758a; display:grid; place-items:center; font-size:18px; cursor:pointer; flex-shrink:0;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
6155              </div>
6156              <div style="padding:20px 22px; display:flex; flex-direction:column; gap:14px;">
6157                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha de subasta *</span><input value="" data-dc-attr-value="{{ rp_fecha }}" onInput="{{ set_rp_fecha }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label>
6158                <button type="button" onClick="{{ toggleRpListoParaCargar }}" aria-pressed="{{ rp_listoParaCargar }}" style="border:1px solid {{ rp_listoBorder }}; border-radius:10px; padding:11px 13px; background:{{ rp_listoBg }}; color:{{ rp_listoColor }}; text-align:left; font:700 13px inherit; cursor:pointer;">{{ rp_listoLabel }}</button>
6159                <sc-if value="{{ rp_showFechaCarga }}" hint-placeholder-val="true"><label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha disponible de carga *</span><input value="" data-dc-attr-value="{{ rp_fechaCarga }}" data-dc-attr-min="{{ rp_fechaCargaMin }}" data-dc-attr-max="{{ rp_fechaCargaMax }}" onInput="{{ set_rp_fechaCarga }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"><span style="font-size:11px; color:#65758a;">Entre 1 y 7 días corridos después de la nueva subasta.</span></label></sc-if>
6160                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Precio sugerido *</span><input value="" data-dc-attr-value="{{ rp_precio }}" onInput="{{ set_rp_precio }}" inputmode="numeric" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label>
6161                <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Plazo de pago *</span><select value="" data-dc-attr-value="{{ rp_plazo }}" onChange="{{ set_rp_plazo }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rpPlazoOpts }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for></select></label>
6162                <sc-if value="{{ rpShowOtro }}" hint-placeholder-val="{{ false }}"><label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Especificar plazo *</span><input value="" data-dc-attr-value="{{ rp_plazoOtro }}" onInput="{{ set_rp_plazoOtro }}" placeholder="Ej: 45 días" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
6162 color:#112033;"></label></sc-if>
6163                <sc-if value="{{ rpMsg }}" hint-placeholder-val="{{ false }}"><div style="font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ rpMsgBg }}; color:{{ rpMsgColor }};">{{ rpMsg }}</div></sc-if>
6164              </div>
6165              <div style="padding:0 22px 20px; display:flex; gap:10px;">
6166                <button onClick="{{ submitRepublish }}" disabled="{{ rpSaving }}" style="flex:1; background:#e63946; color:#fff; border:none; border-radius:10px; padding:12px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer;">{{ rpSaveLabel }}</button>
6167                <button onClick="{{ closeRepublish }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:12px 20px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button>
6168              </div>
6169            </div>
6170          </div>
6171        </sc-if>
6172        </sc-if>
6173        <div style="height:40px;"></div>
6174      </div>
6175    </sc-if>
6176
6177
6178    <sc-if value="{{ isRosganAuditor }}" hint-placeholder-val="{{ false }}">
6179      <main class="ra-route-loading" aria-busy="{{ rgAuditorRouteBusy }}">
6180        <div class="ra-brand">Rosgan · Gestión de remates</div>
6181        <h1>Panel Auditor</h1>
6182        <p role="status">{{ rgAuditorRouteMessage }}</p>
6183        <sc-if value="{{ rgAuditorRouteError }}" hint-placeholder-val="{{ false }}">
6184          <button class="ra-button" onClick="{{ retryRgAuditor }}">Reintentar</button>
6185          <button class="ra-button" onClick="{{ exitRgAuditor }}">Volver a remates</button>
6186        </sc-if>
6187      </main>
6188    </sc-if>
6189    <!-- ============ REMATES (HISTÓRICO) ============ -->
6190    <sc-if value="{{ isRemates }}" hint-placeholder-val="{{ false }}">
6191      <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;">
6192        <sc-if value="{{ isRematesIndex }}" hint-placeholder-val="true">
6193        <div style="padding:26px 0 18px;">
6194          <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin-top:4px;">
6195            <div><h1 style="font-size:clamp(24px, 3.4vw, 34px); font-weight:800; letter-spacing:-0.02em; margin:0; color:#112033;">Remates</h1></div>
6196            <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
6197              <sc-if value="{{ rgCanViewPanel }}" hint-placeholder-val="false"><button type="button" class="rosgan-page-action" onClick="{{ toggleRematePanelPicker }}" aria-haspopup="dialog" aria-controls="remate-panel-picker"><img src="/assets/icons/lucide/clipboard-list.svg" width="17" height="17" alt="" aria-hidden="true">{{ rgPanelLabel }}</button></sc-if>
6198              <sc-if value="{{ canAdminRosganRemates }}" hint-placeholder-val="false"><button onClick="{{ openRosganRemateCreate }}" style="display:inline-flex; align-items:center; gap:7px; border:none; border-radius:12px; padding:10px 15px; background:#e63946; color:#fff; font-family:inherit; font-size:13px; font-weight:800; cursor:pointer; box-shadow:0 8px 20px rgba(230,57,70,.24);"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i>Crear remate</button></sc-if>
6199              <sc-if value="{{ canLoadRosganLot }}" hint-placeholder-val="false"><button onClick="{{ goRosganLotLoad }}" style="display:inline-flex; align-items:center; gap:7px; border:none; border-radius:12px; padding:10px 15px; background:#0a5ca8; color:#fff; font-family:inherit; font-size:13px; font-weight:700; cursor:pointer; box-shadow:0 7px 18px rgba(10,92,168,.22);"><span style="font-size:16px;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote Rosgan</button></sc-if>
6200            </div>
6201          </div>
6202        </div>
6203        <sc-if value="{{ showRosganRemates }}" hint-placeholder-val="true">
6204          <section style="margin-bottom:28px;">
6205            <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:11px;">
6206              <div><div style="font-size:11px; font-weight:850; letter-spacing:.12em; color:#0a5ca8;">PRÓXIMOS REMATES</div><h2 style="font-size:20px; margin:3px 0 0; color:#112033;">Remates activos</h2></div>
6207              <button onClick="{{ refreshRosganRemates }}" disabled="{{ rosganRematesLoading }}" title="Actualizar remates" style="width:40px; height:40px; display:grid; place-items:center; border-radius:12px; border:1px solid #dce7f2; background:#fff; cursor:pointer;"><img src="assets/icons/lucide/refresh-cw.svg" alt="" style="width:17px; height:17px;"></button>
6208            </div>
6209            <sc-if value="{{ rosganRematesInitialLoading }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px;"><sc-for list="{{ skeletons }}" as="k" hint-placeholder-count="3"><div style="height:142px; border-radius:18px; background:linear-gradient(90deg,#e8eef5 25%,#f8fafd 50%,#e8eef5 75%); background-size:800px 100%; animation:shimmer 1.3s linear infinite;"></div></sc-for></div></sc-if>
6210            <sc-if value="{{ rosganRematesHasError }}" hint-placeholder-val="false"><div style="padding:14px 16px; border-radius:14px; border:1px solid #f3c6c9; background:#fff4f4; color:#a72b35; font-size:13px;">{{ rosganRematesError }}</div></sc-if>
6211            <sc-if value="{{ hasRosganRemates }}" hint-placeholder-val="false">
6212              <div class="rosgan-remate-admin-grid rg-active-remates">
6213                <sc-for list="{{ rosganRemateCards }}" as="r" hint-placeholder-count="3">
6214                  <article onClick="{{ r.open }}" tabindex="0" role="button" style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;">
6215                    <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid #e6edf5; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:38px; height:38px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:#0a5ca8;">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:#16846e; font-weight:850; letter-spacing:.09em;">{{ r.status }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div><sc-if value="{{ r.prebidsActive }}" hint-placeholder-val="false"><span class="prebid-active-badge"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas</span></sc-if></div></div>
6216                    <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0;"><sc-if value="{{ r.showFact }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.factLabel }}</small><strong style="display:block; font-size:13px; color:#112033;">{{ r.factValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÓN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div>
6217                  <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article>
6218                </sc-for>
6219              </div>
6220            </sc-if>
6221            <sc-if value="{{ rosganRematesEmpty }}" hint-placeholder-val="false"><div style="padding:24px; text-align:center; border:1px dashed #cdd9e6; border-radius:16px; background:rgba(255,255,255,.7);"><strong style="display:block; color:#112033;">Todavía no hay remates activos</strong><span style="font-size:13px; color:#65758a;">Los próximos remates de Rosgan y cabañas aparecerán acá.</span></div></sc-if>
6222          </section>
6223        </sc-if>
6224        </sc-if>
6225        <sc-if value="{{ rosganRematePageVisible }}" hint-placeholder-val="false">
6226          <button onClick="{{ closeRosganRemateForm }}" style="display:inline-flex; align-items:center; gap:7px; margin:22px 0 12px; padding:0; border:none; background:transparent; color:#0a5ca8; font:inherit; font-size:13px; font-weight:750; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Volver a remates</button>
6227          <section style="background:#fff; border:1px solid #dfe8f2; border-radius:22px; padding:clamp(18px,3vw,28px); margin:0 0 30px; box-shadow:0 18px 50px rgba(0,35,70,.1); animation:pageIn 250ms ease both;">
6228            <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:14px; margin-bottom:22px;"><div><span style="font-size:10.5px; font-weight:850; letter-spacing:.12em; color:#e63946;">{{ rosganRemateFormEyebrow }}</span><h2 style="font-size:24px; margin:4px 0; color:#112033;">{{ rosganRemateFormTitle }}</h2><p style="font-size:13px; color:#65758a; margin:0;">Los campos con asterisco son obligatorios. El flyer se carga al guardar.</p></div><button onClick="{{ closeRosganRemateForm }}" title="Cerrar" style="width:38px; height:38px; border-radius:12px; border:1px solid #dfe7f0; background:#f7f9fc; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div>
6229            <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,520px),1fr)); gap:22px; align-items:start;">
6230              <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr)); gap:14px;">
6231                <label style="grid-column:1/-1; display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">TÍTULO *</span><input value="" data-dc-attr-value="{{ rrfTitle }}" onInput="{{ setRrfTitle }}" placeholder="Ej. Remate Habitual Rosgan" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; outline:none;"></label>
6232                <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">FECHA *</span><input type="date" value="" data-dc-attr-value="{{ rrfDate }}" onInput="{{ setRrfDate }}" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"></label>
6233                <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">HORA *</span><input type="time" value="" data-dc-attr-value="{{ rrfTime }}" onInput="{{ setRrfTime }}" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"></label>
6234                <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">CABEZAS</span><input type="number" min="1" step="1" inputmode="numeric" value="" data-dc-attr-value="{{ rrfHeads }}" onInput="{{ setRrfHeads }}" placeholder="Automático: suma de los lotes" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"><span style="font-size:11px;color:#60758c;">Opcional. Vacío: suma de las cantidades de todos los lotes. Si indicás un valor, se mostrará ese.</span></label>
6235                <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">TIPO DE REMATE *</span><select value="" data-dc-attr-value="{{ rrfType }}" onChange="{{ setRrfType }}" disabled="{{ rrfTypeDisabled }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><option value="">{{ rrfTypePlaceholder }}</option><sc-for list="{{ rosganRemateTypeOptions }}" as="o" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select><sc-if value="{{ rrfTypesMessageVisible }}" hint-placeholder-val="false"><small style="color:#a32934;">{{ rrfTypesMessage }}</small></sc-if></label>
6236                <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">MODO DE CONSIGNATARIA *</span><select value="" data-dc-attr-value="{{ rrfMode }}" onChange="{{ setRrfMode }}" disabled="{{ rrfModeLocked }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><sc-for list="{{ rosganRemateModeOptions }}" as="o" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select><sc-if value="{{ rrfModeLocked }}" hint-placeholder-val="false"><small style="color:#65758a;">Este tipo admite únicamente modo Individual.</small></sc-if></label>
6237                <sc-if value="{{ rrfShowConsignataria }}" hint-placeholder-val="true"><label style="grid-column:1/-1; display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">CONSIGNATARIA *</span><select value="" data-dc-attr-value="{{ rrfConsignataria }}" onChange="{{ setRrfConsignataria }}" disabled="{{ rrfConsignatariaLocked }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><option value="">Seleccionar consignataria…</option><sc-for list="{{ rosganRemateConsignatarias }}" as="o" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select><sc-if value="{{ rrfConsignatariaLocked }}" hint-placeholder-val="false"><small style="color:#65758a;">Asignada automáticamente según tu usuario.</small></sc-if></label></sc-if>
6238                <sc-if value="{{ rrfAllConsignatarias }}" hint-placeholder-val="false"><div style="grid-column:1/-1; padding:11px 13px; border:1px solid #c7e1d9; border-radius:12px; background:#f0faf7; color:#176b58; font-size:12px; font-weight:700;">El remate quedará disponible para todas las consignatarias.</div></sc-if>
6239                <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">PROVINCIA *</span><select value="" data-dc-attr-value="{{ rrfProvince }}" onChange="{{ setRrfProvince }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><option value="">Seleccionar…</option><sc-for list="{{ rosganRemateProvinces }}" as="o" hint-placeholder-count="6"><option value="" data-dc-attr-value="{
6239{ o }}">{{ o }}</option></sc-for></select></label>
6240                <div style="position:relative;display:grid;gap:6px;"><label for="rrf-locality" style="display:flex;justify-content:space-between;font-size:11px;font-weight:800;color:#526174;">LOCALIDAD *<span style="display:inline-flex;gap:4px;align-items:center;font-weight:500;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;">Google</span></label><input id="rrf-locality" role="combobox" aria-autocomplete="list" aria-controls="rrf-place-options" aria-expanded="{{ rrfPlacesExpanded }}" aria-activedescendant="{{ rrfPlaceActiveId }}" value="" data-dc-attr-value="{{ rrfLocality }}" onInput="{{ setRrfLocality }}" onKeyDown="{{ rrfPlaceKey }}" onFocus="{{ rrfPlaceFocus }}" onBlur="{{ rrfPlaceBlur }}" disabled="{{ rrfLocalityDisabled }}" autocomplete="off" placeholder="{{ rrfLocalityPlaceholder }}" style="min-width:0;width:100%;box-sizing:border-box;border:1px solid #cedbea;border-radius:12px;padding:12px 13px;font:inherit;background:#fff;"><sc-if value="{{ rrfPlacesVisible }}"><div id="rrf-place-options" role="listbox" aria-label="Localidades sugeridas" style="position:absolute;z-index:45;left:0;right:0;top:100%;max-height:240px;overflow:auto;padding:5px;border:1px solid #cedbea;border-radius:12px;background:#fff;box-shadow:0 12px 28px rgba(0,30,60,.16);"><sc-for list="{{ rrfPlaces }}" as="place"><button type="button" role="option" id="{{ place.id }}" aria-selected="{{ place.selected }}" onPointerDown="{{ place.pickPointer }}" onClick="{{ place.pick }}" style="width:100%;padding:10px;border:0;border-radius:8px;background:{{ place.bg }};text-align:left;cursor:pointer;"><strong style="display:block;color:#123553;">{{ place.label }}</strong><small style="display:block;color:#60758c;margin-top:3px;">{{ place.address }} · {{ place.source }}</small></button></sc-for></div></sc-if><span role="status" style="font-size:11px;color:#60758c;">{{ rrfPlaceStatus }}</span></div>
6241                <label style="grid-column:1/-1; display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">LUGAR</span><input value="" data-dc-attr-value="{{ rrfPlace }}" onInput="{{ setRrfPlace }}" placeholder="Predio, sociedad rural o establecimiento" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"></label>
6242                <div style="grid-column:1/-1;"><span style="font-size:11px; font-weight:800; color:#526174; display:block; margin-bottom:8px;">CONFIGURACIÓN OPERATIVA</span><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:8px;"><sc-for list="{{ rosganRemateToggles }}" as="t" hint-placeholder-count="7"><button onClick="{{ t.toggle }}" type="button" style="display:flex; align-items:center; gap:10px; padding:10px 11px; border-radius:11px; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:#26384d; font:inherit; font-size:12px; font-weight:700; cursor:pointer; text-align:left;"><span style="width:34px; height:19px; padding:2px; box-sizing:border-box; background:{{ t.track }}; border-radius:20px; flex:none;"><span style="display:block; width:15px; height:15px; border-radius:50%; background:#fff; transform:translateX({{ t.knob }}); transition:transform .18s;"></span></span>{{ t.label }}</button></sc-for></div></div>
6243              </div>
6244              <aside style="border:1px solid #dce7f2; border-radius:17px; background:#f7fafd; padding:14px; align-self:start;"><div style="font-size:11px; font-weight:800; color:#526174; margin-bottom:9px;">FLYER DEL REMATE</div><label style="display:grid; place-items:center; min-height:230px; border:1px dashed #aac2d9; border-radius:14px; background:#fff; overflow:hidden; cursor:pointer; text-align:center; padding:12px; box-sizing:border-box;"><input type="file" accept="image/png,image/jpeg,image/webp" onChange="{{ setRosganRemateFlyer }}" style="display:none;"><sc-if value="{{ rrfHasFlyer }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ rrfFlyerPreview }}" alt="Previsualización del flyer" style="width:100%; height:230px; object-fit:contain;"></sc-if><sc-if value="{{ rrfNoFlyer }}" hint-placeholder-val="true"><span><img src="assets/icons/lucide/image-plus.svg" alt="" style="width:28px; height:28px; opacity:.65;"><strong style="display:block; color:#112033; margin-top:9px;">Elegir flyer</strong><small style="display:block; color:#7a8ca0; margin-top:4px;">PNG, JPG o WebP</small></span></sc-if></label><div style="font-size:11.5px; color:#65758a; margin-top:9px; line-height:1.4;">Podés crear el remate sin flyer y agregarlo luego desde Editar.</div></aside>
6245            </div>
6246            <sc-if value="{{ rosganRemateHasMessage }}" hint-placeholder-val="false"><div style="margin-top:16px; padding:12px 14px; border-radius:12px; border:1px solid {{ rosganRemateMessageBorder }}; background:{{ rosganRemateMessageBg }}; color:{{ rosganRemateMessageColor }}; font-size:13px;">{{ rosganRemateMessageText }}</div></sc-if>
6247            <div style="display:flex; justify-content:flex-end; gap:9px; margin-top:20px; padding-top:18px; border-top:1px solid #edf2f7;"><button onClick="{{ closeRosganRemateForm }}" disabled="{{ rosganRemateSaving }}" style="padding:11px 17px; border-radius:11px; border:1px solid #cedbea; background:#fff; font:inherit; font-weight:700; cursor:pointer;">Cancelar</button><button onClick="{{ saveRosganRemate }}" disabled="{{ rosganRemateSaving }}" style="padding:11px 19px; border-radius:11px; border:none; background:#0a5ca8; color:#fff; font:inherit; font-weight:800; cursor:pointer; box-shadow:0 8px 18px rgba(10,92,168,.2);">{{ rosganRemateSaveLabel }}</button></div>
6248          </section>
6249        </sc-if>
6250        <sc-if value="{{ hasScheduledRemates }}" hint-placeholder-val="false">
6251          <section style="margin:26px 0 30px;"><div style="font-size:11px;font-weight:850;letter-spacing:.12em;color:#0a5ca8;">AGENDA DE REMATES</div><h2 style="font-size:20px;margin:4px 0 16px;color:#112033;">Próximos remates</h2><div class="rosgan-remate-admin-grid">
6252              <sc-for list="{{ scheduledRemateCards }}" as="r" hint-placeholder-count="6">
6253                <article onClick="{{ r.open }}" tabindex="0" role="button" data-reveal style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;">
6254                  <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid {{ r.logoBorder }}; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:28px; height:28px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:{{ r.color }};">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:{{ r.color }}; font-weight:850; letter-spacing:.09em;">{{ r.type }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div><sc-if value="{{ r.prebidsActive }}" hint-placeholder-val="false"><span class="prebid-active-badge"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas</span></sc-if></div></div>
6255                  <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0 0;"><sc-if value="{{ r.showMeta }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.metaLabel }}</small><strong style="display:block; font-size:12px; color:#112033;">{{ r.metaValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÓN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div>
6256                <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article>
6257              </sc-for>
6258            </div>
6259          </section>
6260        </sc-if>
6261        <section style="margin:28px 0;" aria-label="Últimos remates">
6262          <h2 style="font-size:20px;margin:0 0 16px;color:#112033;">Últimos remates</h2>
6263          <sc-if value="{{ latestFinishedLoading }}" hint-placeholder-val="false"><p style="color:#718196;">Cargando últimos remates…</p></sc-if>
6264          <sc-if value="{{ latestFinishedError }}" hint-placeholder-val="false"><p style="color:#a32934;">{{ latestFinishedError }} <button type="button" onClick="{{ retryLatestFinished }}">Reintentar</button></p></sc-if>
6265          <sc-if value="{{ hasLatestFinished }}" hint-placeholder-val="false"><div class="rosgan-remate-admin-grid">
6266              <sc-for list="{{ latestFinishedCards }}" as="r" hint-placeholder-count="6">
6267                <article onClick="{{ r.open }}" tabindex="0" role="button" data-reveal style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;">
6268                  <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid {{ r.logoBorder }}; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:28px; height:28px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:{{ r.color }};">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:{{ r.color }}; font-weight:850; letter-spacing:.09em;">{{ r.type }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div></div></div>
6269                  <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0 0;"><sc-if value="{{ r.showMeta }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.metaLabel }}</small><strong style="display:block; font-size:12px; color:#112033;">{{ r.metaValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÓN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div>
6270                <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article>
6271              </sc-for>
6272          </div></sc-if>
6273        </section>
6274        <sc-if value="{{ showRemateArchiveToggle }}" hint-placeholder-val="false"><button class="remate-past-toggle" type="button" onClick="{{ toggleRemateArchive }}" aria-expanded="{{ remateArchiveOpen }}"><span class="remate-past-icon"><img src="assets/icons/lucide/file-text.svg" alt=""></span><span>{{ remateArchiveToggleLabel }}</span><img class="remate-past-chevron" src="assets/icons/lucide/chevron-down.svg" alt=""></button></sc-if>
6275        <sc-if value="{{ showRemateArchive }}" hint-placeholder-val="false">
6276        <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin:0 0 16px; padding-top:20px; border-top:1px solid #dfe8f1;"><div><div style="font-size:10.5px; letter-spacing:.12em; color:#7c8da2; font-weight:800;">ARCHIVO HISTÓRICO</div><h2 style="font-size:20px; margin:3px 0 0; color:#112033;">Remates anteriores</h2><p style="margin:4px 0 0; color:#718196; font-size:11.5px;">Historial unificado de Rosgan y cabañas</p></div><div style="display:flex; align-items:center; gap:8px;"><span style="font-size:12.5px; color:#65758a; font-weight:600;">Año</span><select value="" data-dc-attr-value="{{ remateYear }}" onChange="{{ onRemateYear }}" style="background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:10px 14px; font-size:14px; font-weight:700; font-family:inherit; color:#112033; outline:none;"><sc-for list="{{ remateYears }}" as="y" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ y }}">{{ y }}</option></sc-for></select></div></div>
6277        <sc-if value="{{ rematesLoadingVisible }}" hint-placeholder-val="{{ false }}">
6278          <div aria-label="Cargando remates" style="animation:pageIn 260ms ease both;">
6279            <div style="display:flex; gap:8px; margin-bottom:18px;">
6280              <span style="width:112px; height:36px; border-radius:100px; background:linear-gradient(90deg,#e8eef5 25%,#f7fafd 50%,#e8eef5 75%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></span>
6281              <span style="width:96px; height:36px; border-radius:100px; background:linear-gradient(90deg,#e8eef5 25%,#f7fafd 50%,#e8eef5 75%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></span>
6282              <span style="width:124px; height:36px; border-radius:100px; background:linear-gradient(90deg,#e8eef5 25%,#f7fafd 50%,#e8eef5 75%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></span>
6283            </div>
6284            <div class="rosgan-remate-admin-grid">
6285              <sc-for list="{{ skeletons }}" as="k" hint-placeholder-count="6">
6286                <div style="height:142px; border-radius:18px; background:linear-gradient(90deg,#e8eef5 25%,#f8fafd 50%,#e8eef5 75%); background-size:800px 100%; animation:shimmer 1.3s linear infinite; box-shadow:0 8px 26px rgba(0,30,60,0.05);"></div>
6287              </sc-for>
6288            </div>
6289          </div>
6290        </sc-if>
6291        <sc-if value="{{ rematesReady }}" hint-placeholder-val="{{ true }}">
6292          <!-- type filter chips -->
6293          <sc-if value="{{ remateFiltersVisible }}" hint-placeholder-val="{{ true }}">
6294            <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px;">
6295              <sc-for list="{{ remateTypeChips }}" as="t" hint-placeholder-count="6">
6296                <span onClick="{{ t.go }}" style="display:inline-flex; align-items:center; gap:8px; padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:{{ t.color }}; transition:transform 160ms;" style-hover="transform:translateY(-2px);"><span style="width:9px; height:9px; border-radius:50%; background:{{ t.dot }};"></span>{{ t.label }}<strong style="font-variant-numeric:tabular-nums;">{{ t.count }}</strong></span>
6297              </sc-for>
6298            </div>
6299          </sc-if>
6300          <sc-if value="{{ hasRemates }}" hint-placeholder-val="{{ true }}">
6301            <div class="rosgan-remate-admin-grid">
6302              <sc-for list="{{ remateCards }}" as="r" hint-placeholder-count="6">
6303                <article onClick="{{ r.open }}" tabindex="0" role="button" data-reveal style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;">
6304                  <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid {{ r.logoBorder }}; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:28px; height:28px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:{{ r.color }};">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:{{ r.color }}; font-weight:850; letter-spacing:.09em;">{{ r.type }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div></div></div>
6305                  <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0 0;"><sc-if value="{{ r.showMeta }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.metaLabel }}</small><strong style="display:block; font-size:12px; color:#112033;">{{ r.metaValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÓN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div>
6306                <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article>
6307              </sc-for>
6308            </div>
6309          </sc-if>
6310          <sc-if value="{{ remateArchiveHasMore }}" hint-placeholder-val="false"><button type="button" onClick="{{ moreRemateArchive }}" style="display:block;margin:20px auto;padding:12px 24px;border:1px solid #cbdced;border-radius:12px;background:#fff;color:#075ca8;font:700 14px inherit;cursor:pointer;">Ver más remates anteriores</button></sc-if>
6311          <sc-if value="{{ rematesEmpty }}" hint-placeholder-val="{{ false }}">
6312            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
6313              <div style="font-size:16px; font-weight:700; color:#112033;">Sin remates para este filtro</div>
6314              <div style="font-size:13.5px; margin-top:4px;">Probá otro año o quitá el filtro de tipo.</div>
6315            </div>
6316          </sc-if>
6317        </sc-if>
6318        </sc-if>
6319        <div style="height:40px;"></div>
6320      </div>
6321    </sc-if>
6322
6323    <!-- ============ CANAL CONSIGNATARIA ============ -->
6324    <sc-if value="{{ isChannel }}" hint-placeholder-val="{{ false }}">
6325      <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); min-height:100vh; background:#f4f7fb;">
6326        <!-- HERO BRANDED -->
6327        <div style="position:relative; overflow:hidden; color:#fff; background:{{ chColor }};">
6328          <sc-for list="{{ chHeroImages }}" as="img" hint-placeholder-count="1">
6329            <div style="position:absolute; inset:0; background-image:url('{{ img.src }}'); background-size:cover; background-position:{{ img.position }}; opacity:{{ img.opacity }}; transition:opacity 1100ms ease; animation:kenburns 12s ease-in-out infinite alternate;"></div>
6330          </sc-for>
6331          <div style="position:absolute; inset:0; background:{{ chHeroOverlay }}; background-size:220% 220%; animation:gradientShift 14s ease infinite;"></div>
6332          <div style="position:absolute; width:420px; height:420px; border-radius:50%; top:-160px; right:-120px; background:radial-gradient(circle, {{ chGlow }}, transparent 68%); opacity:0.4; animation:gradientBlob 18s ease-in-out infinite; pointer-events:none;"></div>
6333          <div style="position:relative; max-width:1240px; margin:0 auto; padding:16px clamp(14px, 4vw, 36px) clamp(24px, 3.4vw, 34px);">
6334            <span onClick="{{ goHome }}" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:rgba(255,255,255,0.82); font-weight:600; cursor:pointer;" style-hover="color:#fff;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al inicio</span>
6335            <div style="display:flex; align-items:center; gap:18px; flex-wrap:wrap; margin-top:20px;">
6336              <span style="width:88px; height:88px; border-radius:22px; background:#fff; display:grid; place-items:center; flex-shrink:0; box-shadow:0 18px 44px rgba(0,0,0,0.28); overflow:hidden; animation:spinIn 620ms cubic-bezier(0.22,0.72,0.18,1) both;">
6337                <sc-if value="{{ chHasLogo }}" hint-placeholder-val="{{ false }}"><div style="width:100%; height:100%; background-image:url('{{ chLogo }}'); background-size:contain; background-position:center; background-repeat:no-repeat; padding:12px; box-sizing:border-box;"></div></sc-if>
6338                <sc-if value="{{ chNoLogo }}" hint-placeholder-val="{{ true }}"><span style="font-size:34px; font-weight:900; color:{{ chColor }};">{{ chInitial }}</span></sc-if>
6339              </span>
6340              <div style="flex:1; min-width:240px; animation:floatIn 560ms cubic-bezier(0.22,0.72,0.18,1) 80ms both;">
6341                <div style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.18); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.28); padding:5px 13px; border-radius:100px; font-size:11px; font-weight:800; letter-spacing:0.08em; margin-bottom:9px;"><span style="width:6px; height:6px; border-radius:50%; background:#fff;"></span>CANAL OFICIAL</div>
6342                <h1 style="font-size:clamp(26px, 4.2vw, 44px); font-weight:900; letter-spacing:-0.025em; margin:0; line-height:1.02; text-wrap:balance;">{{ chName }}</h1>
6343                <div style="font-size:14px; color:rgba(255,255,255,0.85); margin-top:8px;">Remates, transmisiones en vivo y grabaciones de la consignataria.</div>
6344              </div>
6345            </div>
6346            <div style="display:grid; grid-template-columns:repeat(3, 1fr); gap:12px; margin-top:24px; max-width:520px;">
6347              <sc-for list="{{ chStats }}" as="st" hint-placeholder-count="3">
6348                <div style="background:rgba(255,255,255,0.12); backdrop-filter:blur(10px); border:1px solid rgba(255,255,255,0.2); border-radius:15px; padding:14px 16px; animation:floatIn 560ms cubic-bezier(0.22,0.72,0.18,1) both;"><div style="font-size:clamp(22px, 3vw, 30px); font-weight:900; font-variant-numeric:tabular-nums; letter-spacing:-0.02em;">{{ st.value }}</div><div style="font-size:10.5px; color:rgba(255,255,255,0.78); letter-spacing:0.07em; margin-top:2px;">{{ st.label }}</div></div>
6349              </sc-for>
6350            </div>
6351            <sc-if value="{{ chHasHeroDots }}" hint-placeholder-val="{{ false }}">
6352              <div style="display:flex; gap:7px; align-items:center; margin-top:20px;">
6353                <sc-for list="{{ chHeroDots }}" as="d" hint-placeholder-count="5">
6354                  <span onClick="{{ d.go }}" style="height:6px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:all 300ms;"></span>
6355                </sc-for>
6356              </div>
6357            </sc-if>
6358          </div>
6359        </div>
6360
6361        <div style="max-width:1240px; margin:0 auto; padding:clamp(20px, 3vw, 30px) clamp(14px, 4vw, 36px) 120px;">
6362          <!-- loading -->
6363          <sc-if value="{{ chLoading }}" hint-placeholder-val="{{ false }}">
6364            <div style="display:flex; align-items:center; gap:12px; color:#65758a; font-size:14px; padding:40px 0;"><span style="width:22px; height:22px; border:3px solid #dbe6f1; border-top-color:{{ chColor }}; border-radius:50%; animation:spin 800ms linear infinite;"></span>Cargando remates del canal…</div>
6365          </sc-if>
6366          <!-- empty -->
6367          <sc-if value="{{ chEmpty }}" hint-placeholder-val="{{ false }}">
6368            <div style="text-align:center; padding:64px 20px; background:#fff; border-radius:20px; box-shadow:0 10px 34px rgba(0,30,60,0.07);">
6369              <div style="width:64px; height:64px; border-radius:18px; background:{{ chColor }}; opacity:0.14; margin:0 auto 16px;"></div>
6370              <div style="font-size:17px; font-weight:800; color:#112033;">Todavía no hay remates publicados</div>
6371              <div style="font-size:13.5px; color:#65758a; margin-top:6px;">Cuando esta consignataria transmita un remate, vas a verlo acá.</div>
6372            </div>
6373          </sc-if>
6374
6375          <!-- featured -->
6376          <sc-if value="{{ chHasFeatured }}" hint-placeholder-val="{{ false }}">
6377            <div onClick="{{ chFeat.open }}" style="position:relative; border-radius:24px; overflow:hidden; cursor:pointer; box-shadow:0 20px 54px rgba(0,30,60,0.16); margin-bottom:26px; aspect-ratio:16/7; min-height:220px; animation:fadeScale 520ms cubic-bezier(0.22,0.72,0.18,1) both;" style-hover="box-shadow:0 30px 70px rgba(0,30,60,0.24);">
6378              <div style="position:absolute; inset:0; background-image:url('{{ chFeat.thumb }}'); background-size:cover; background-position:{{ chFeat.thumbPosition }}; transition:transform 700ms cubic-bezier(0.22,0.72,0.18,1);"></div>
6379              <div style="position:absolute; inset:0; background:linear-gradient(105deg, rgba(0,20,40,0.9) 8%, rgba(0,20,40,0.45) 48%, transparent 82%);"></div>
6380              <div style="position:absolute; inset:0; padding:clamp(20px, 3vw, 34px); display:flex; flex-direction:column; justify-content:flex-end; gap:10px;">
6381                <h2 style="color:#fff; font-size:clamp(20px, 3.2vw, 32px); font-weight:900; margin:0; line-height:1.08; letter-spacing:-0.02em; max-width:720px; text-shadow:0 2px 14px rgba(0,0,0,0.4);">{{ chFeat.title }}</h2>
6382                <div style="color:rgba(255,255,255,0.88); font-size:13.5px; font-weight:600;"><i class="dc-lucide dc-lucide-calendar" aria-hidden="true"></i> {{ chFeat.date }}<sc-if value="{{ chFeat.loc }}" hint-placeholder-val="{{ false }}"> · <i class="dc-lucide dc-lucide-map-pin" aria-hidden="true"></i> {{ chFeat.loc }}</sc-if></div>
6383              </div>
6384              <span style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:56px; height:56px; border-radius:50%; background:rgba(255,255,255,0.18); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:20px; padding-left:4px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span>
6385            </div>
6386          </sc-if>
6387
6388          <!-- tabs -->
6389          <sc-if value="{{ chHasFeatured }}" hint-placeholder-val="{{ false }}">
6390            <div style="display:flex; gap:9px; flex-wrap:wrap; margin-bottom:18px;">
6391              <sc-for list="{{ chTabs }}" as="t" hint-placeholder-count="4">
6392                <span onClick="{{ t.go }}" style="display:inline-flex; align-items:center; gap:8px; padding:9px 16px; border-radius:100px; font-size:13px; font-weight:700; cursor:pointer; background:{{ t.bg }}; color:{{ t.color }}; border:1px solid {{ t.border }}; transition:all 220ms;">
6393                  <sc-if value="{{ t.live }}" hint-placeholder-val="{{ false }}"><span style="width:7px; height:7px; border-radius:50%; background:currentColor;"></span></sc-if>
6394                  {{ t.label }}<strong style="font-variant-numeric:tabular-nums; opacity:0.7;">{{ t.count }}</strong>
6395                </span>
6396              </sc-for>
6397            </div>
6398          </sc-if>
6399
6400          <!-- grid -->
6401          <sc-if value="{{ chHasCards }}" hint-placeholder-val="{{ false }}">
6402            <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(clamp(240px, 30vw, 300px), 1fr)); gap:16px;">
6403              <sc-for list="{{ chCards }}" as="m" hint-placeholder-count="6">
6404                <div data-reveal onClick="{{ m.open }}" style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.09); cursor:pointer; transition:transform 240ms cubic-bezier(0.22,0.72,0.18,1), box-shadow 240ms;" style-hover="transform:translateY(-5px); box-shadow:0 22px 50px rgba(0,30,60,0.2);">
6405                  <div style="position:relative; aspect-ratio:16/9;">
6406                    <div style="position:absolute; inset:0; background-image:url('{{ m.thumb }}'); background-size:cover; background-position:{{ m.thumbPosition }};"></div>
6407                    <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,40,0) 42%, rgba(0,20,40,0.78));"></div>
6408                    <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:46px; height:46px; border-radius:50%; background:rgba(255,255,255,0.16); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:16px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span>
6409                  </div>
6410                  <div style="padding:13px 15px;">
6411                    <strong style="font-size:15px; font-weight:800; color:#112033; line-height:1.2; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ m.title }}</strong>
6412                    <div style="font-size:12.5px; color:#65758a; margin-top:6px;">{{ m.date }}</div>
6413                  </div>
6414                </div>
6415              </sc-for>
6416            </div>
6417          </sc-if>
6418
6419          <!-- videos del canal (YouTube asociado por remate) -->
6420          <sc-if value="{{ chHasVideos }}" hint-placeholder-val="{{ false }}">
6421            <div style="margin-top:34px;">
6422              <div style="display:flex; align-items:center; gap:9px; margin-bottom:16px;">
6423                <span style="width:26px; height:20px; border-radius:5px; background:{{ chColor }}; display:grid; place-items:center; color:#fff; font-size:10px; padding-left:2px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span>
6424                <div style="font-size:11.5px; letter-spacing:0.14em; color:{{ chColor }}; font-weight:800;">PLAYLIST DEL CANAL</div>
6425              </div>
6426              <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(240px, 26vw, 320px); gap:16px; overflow-x:auto; padding-bottom:14px; scroll-snap-type:x mandatory; scrollbar-width:none;">
6427                <sc-for list="{{ chVideos }}" as="v" hint-placeholder-count="4">
6428                  <a href="{{ v.href }}" target="_blank" rel="noopener" data-reveal style="scroll-snap-align:start; background:#fff; border-radius:16px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.09); color:#112033; display:block; transition:transform 220ms, box-shadow 220ms;" style-hover="transform:translateY(-4px); box-shadow:0 20px 46px rgba(0,30,60,0.18); color:#112033;">
6429                    <div style="aspect-ratio:16/9; background-image:url('{{ v.thumb }}'); background-size:cover; background-position:center; position:relative;">
6430                      <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,40,0) 55%, rgba(0,20,40,0.5));"></div>
6431                      <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:44px; height:44px; border-radius:50%; background:rgba(0,29,61,0.72); display:grid; place-items:center; color:#fff; font-size:15px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span>
6432                    </div>
6433                    <div style="padding:12px 14px;"><strong style="font-size:14px; line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ v.title }}</strong><div style="font-size:12px; color:#8593a6; margin-top:5px;">{{ v.meta }}</div></div>
6434                  </a>
6435                </sc-for>
6436              </div>
6437            </div>
6438          </sc-if>
6439
6440          <!-- videoteca por año -->
6441          <sc-if value="{{ chHasLibrary }}" hint-placeholder-val="{{ false }}">
6442            <div style="margin-top:34px;">
6443              <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:16px;">
6444                <div>
6445                  <div style="font-size:11.5px; letter-spacing:0.14em; color:{{ chColor }}; font-weight:800;">VIDEOTECA</div>
6446                  <h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:800; margin:2px 0 0; color:#112033;">Remates grabados por año</h2>
6447                </div>
6448                <span style="font-size:12.5px; color:#8593a6; font-weight:600;">{{ chLibCount }}</span>
6449              </div>
6450              <div style="display:flex; gap:9px; flex-wrap:wrap; margin-bottom:18px;">
6451                <sc-for list="{{ chYearTabs }}" as="y" hint-placeholder-count="3">
6452                  <span onClick="{{ y.go }}" style="padding:8px 18px; border-radius:100px; font-size:13.5px; font-weight:{{ y.weight }}; cursor:pointer; background:{{ y.bg }}; color:{{ y.color }}; border:1px solid {{ y.border }}; font-variant-numeric:tabular-nums; transition:all 220ms;">{{ y.label }}</span>
6453                </sc-for>
6454              </div>
6455              <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(clamp(200px, 24vw, 250px), 1fr)); gap:14px;">
6456                <sc-for list="{{ chLibrary }}" as="m" hint-placeholder-count="4">
6457                  <div data-reveal onClick="{{ m.open }}" style="background:#fff; border-radius:16px; overflow:hidden; box-shadow:0 6px 22px rgba(0,30,60,0.08); cursor:pointer; transition:transform 220ms, box-shadow 220ms;" style-hover="transform:translateY(-4px); box-shadow:0 18px 44px rgba(0,30,60,0.17);">
6458                    <div style="position:relative; aspect-ratio:16/9;">
6459                      <div style="position:absolute; inset:0; background-image:url('{{ m.thumb }}'); background-size:cover; background-position:{{ m.thumbPosition }};"></div>
6460                      <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,40,0) 55%, rgba(0,20,40,0.62));"></div>
6461                      <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,0.18); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:14px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span>
6462                    </div>
6463                    <div style="padding:11px 13px;"><strong style="font-size:13.5px; font-weight:800; color:#112033; line-height:1.25; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ m.title }}</strong></div>
6464                  </div>
6465                </sc-for>
6466              </div>
6467            </div>
6468          </sc-if>
6469        </div>
6470      </div>
6471    </sc-if>
6472
6473    <!-- ============ REMATE DETALLE ? CATÁLOGO ============ -->
6474    <sc-if value="{{ isRosganResults }}" hint-placeholder-val="{{ false }}">
6475      <main style="max-width:1240px;margin:0 auto;padding:26px clamp(14px,4vw,36px) 60px;">
6476        <a class="rosgan-page-action" href="{{ resultsBackHref }}">← Volver al remate</a>
6477        <rosgan-auction-results auction-key="{{ resultsPageKey }}"></rosgan-auction-results>
6478      </main>
6479    </sc-if>
6480    <sc-if value="{{ isRemateDetail }}" hint-placeholder-val="{{ false }}">
6481      <div>
6482        <sc-if value="{{ rdCatalogPage }}" hint-placeholder-val="false">
6483        <!-- HERO -->
6484        <div style="position:relative; overflow:hidden; background:linear-gradient(120deg, #001d3d 0%, #0a3e78 65%, {{ rdColor }} 140%); color:#fff;">
6485          <div style="position:absolute; right:-60px; top:-60px; width:280px; height:280px; border-radius:50%; background:radial-gradient(circle, rgba(255,255,255,0.08), transparent 70%);"></div>
6486          <div style="position:relative; max-width:1240px; margin:0 auto; padding:18px clamp(14px, 4vw, 36px) clamp(20px, 3vw, 30px);">
6487            <span onClick="{{ goRemates }}" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:#c8d8ea; font-weight:600; cursor:pointer;" style-hover="color:#fff;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver a remates</span>
6488            <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap; margin-top:16px;">
6489              <span style="width:62px; height:62px; border-radius:16px; background:{{ rdLogoBg }}; border:1px solid {{ rdLogoBorder }}; display:grid; place-items:center; flex-shrink:0; box-shadow:0 10px 30px rgba(0,0,0,0.25); overflow:hidden;">
6490                <sc-if value="{{ rdHasLogo }}" hint-placeholder-val="{{ false }}"><div style="width:38px; height:38px; background-image:url('{{ rdLogo }}'); background-size:contain; background-position:center; background-repeat:no-repeat;"></div></sc-if>
6491                <sc-if value="{{ rdNoLogo }}" hint-placeholder-val="{{ true }}"><span style="font-size:24px; font-weight:800; color:{{ rdColor }};">{{ rdInitial }}</span></sc-if>
6492              </span>
6493              <div style="flex:1; min-width:220px;">
6494                <span style="display:inline-block; font-size:10.5px; font-weight:800; letter-spacing:0.1em; background:{{ rdColor }}; padding:4px 11px; border-radius:100px; margin-bottom:6px;">{{ rdType }}</span>
6495                <h1 style="font-size:clamp(23px, 3.4vw, 34px); font-weight:800; letter-spacing:-0.02em; margin:0; line-height:1.08;">{{ rdTitle }}</h1>
6496                <div style="font-size:13.5px; color:#c8d8ea; margin-top:5px;"><i class="dc-lucide dc-lucide-calendar" aria-hidden="true"></i> {{ rdDate }} · <i class="dc-lucide dc-lucide-map-pin" aria-hidden="true"></i> {{ rdLocation }} · {{ rdConsignataria }}</div>
6497              </div>
6498              <div class="rd-hero-actions" style="display:flex; gap:8px; margin-left:auto; align-self:flex-start;">
6499              <button type="button" onClick="{{ shareCurrentRemate }}" disabled="{{ !rdShareReady }}" style="display:inline-flex;align-items:center;gap:7px;min-height:35px;padding:9px 12px;border:1px solid rgba(255,255,255,.34);border-radius:11px;background:rgba(255,255,255,.12);color:#fff;font:750 12px inherit;cursor:pointer;"><img src="/assets/icons/lucide/share-2.svg" alt="" aria-hidden="true" width="15" height="15" style="filter:brightness(0) invert(1)">{{ rdShareLabel }}</button>
6500              <sc-if value="{{ rdCanAdminRemate }}" hint-placeholder-val="false">
6501                <div style="display:contents;">
6502                  <button onClick="{{ editCurrentRosganRemate }}" style="display:inline-flex; align-items:center; gap:7px; border:1px solid rgba(255,255,255,.34); border-radius:11px; padding:9px 12px; background:rgba(255,255,255,.12); color:#fff; font:750 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/pencil.svg" alt="" aria-hidden="true" width="15" height="15" style="display:block; width:15px; height:15px; filter:brightness(0) invert(1);">Editar remate</button>
6503                  <button onClick="{{ deleteCurrentRosganRemate }}" disabled="{{ rdRemateDeleting }}" style="display:inline-flex; align-items:center; gap:7px; border:1px solid rgba(255,170,178,.42); border-radius:11px; padding:9px 12px; background:rgba(190,31,48,.24); color:#fff; font:750 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/trash-2.svg" alt="" aria-hidden="true" width="15" height="15" style="display:block; width:15px; height:15px; filter:brightness(0) invert(1);">{{ rdDeleteRemateLabel }}</button>
6504                </div>
6505              </sc-if>
6506              </div>
6507            </div>
6508            <sc-if value="{{ rdHasCategoryHeads }}" hint-placeholder-val="false">
6509              <div style="margin-top:14px;">
6510                <div style="font-size:10.5px; font-weight:850; letter-spacing:.1em; color:#9fc0e2; margin-bottom:8px;">CABEZAS POR CATEGORÍA</div>
6511                <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr)); gap:8px;">
6512                  <sc-for list="{{ rdCategoryHeads }}" as="category" hint-placeholder-count="6">
6513                    <button type="button" onClick="{{ category.pick }}" aria-pressed="{{ category.active }}" style="min-width:0; display:flex; align-items:center; justify-content:space-between; gap:10px; background:{{ category.bg }}; border:1px solid {{ category.border }}; border-radius:11px; padding:9px 11px; color:inherit; font:inherit; text-align:left; cursor:pointer;">
6514                      <span style="min-width:0; font-size:10.5px; line-height:1.15; font-weight:750; color:#d9e7f5; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;" title="{{ category.label }}">{{ category.label }}</span>
6515                      <strong style="flex:none; font-size:15px; font-weight:900; font-variant-numeric:tabular-nums;">{{ category.value }}</strong>
6516                    </button>
6517                  </sc-for>
6518                </div>
6519              </div>
6520            </sc-if>
6521          </div>
6522        </div>
6523
6524        <div style="max-width:1240px; margin:0 auto; padding:0 clamp(14px, 4vw, 36px);">
6525          <a class="rosgan-page-action" href="{{ rdResultsHref }}" style="display:inline-flex;margin:20px 0 6px;"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="">Resultados</a>
6526          <!-- TOOLBAR -->
6527          <div class="rd-responsive-toolbar" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; position:sticky; top:{{ poolBarTop }}; z-index:30; background:rgba(244,247,251,0.94); backdrop-filter:blur(12px); padding:14px 0; border-bottom:1px solid #e3eaf2;">
6528            <div class="rd-search-field" style="flex:1;
6528 min-width:200px; display:flex; align-items:center; gap:10px; background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:11px 14px; box-shadow:0 4px 14px rgba(0,30,60,0.05);">
6529              <span style="color:#8593a6; font-size:15px;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span>
6530              <input value="" data-dc-attr-value="{{ rdQuery }}" onInput="{{ onRdQuery }}" placeholder="Buscar por lote, certificado, categoría, raza o localidad…" style="border:none; outline:none; font-size:14px; font-family:inherit; width:100%; background:none; color:#112033;">
6531            </div>
6532            <button onClick="{{ toggleRdFilters }}" aria-expanded="{{ rdFiltersOpen }}" aria-controls="remate-lot-filters" style="display:flex;align-items:center;gap:8px;border:1px solid #cbdff1;border-radius:100px;background:white;color:#003767;padding:12px 16px;font:700 13px inherit;cursor:pointer;"><img src="/assets/icons/lucide/sliders-horizontal.svg" width="16" height="16" alt="">{{ rdFilterButton }}</button>
6533            <sc-if value="{{ rdCatalogPublished }}"><button type="button" class="remate-list-catalog rd-catalog-cta" onClick="{{ openRdCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="" aria-hidden="true"><span>Catálogo</span></button></sc-if>
6534            <sc-if value="{{ rdCompanyCatalogAvailable }}"><button type="button" class="remate-list-catalog rd-catalog-cta rd-company-catalog" style="--company-color:{{ rdCompanyCatalogColor }}" onClick="{{ openRdCompanyCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt=""><span>Catálogo por consignataria</span></button></sc-if>
6535            <sc-if value="{{ rdCanViewAuditor }}" hint-placeholder-val="false">
6536              <button class="rosgan-auditor-entry" onClick="{{ openRdAuditor }}"><img src="/assets/icons/lucide/clipboard-list.svg" width="17" height="17" alt="" aria-hidden="true" style="display:block;flex-shrink:0;filter:brightness(0) invert(1);">{{ rdAuditorLabel }}</button>
6537            </sc-if>
6538            <sc-if value="{{ rdCanManageSalesOrder }}" hint-placeholder-val="false">
6539              <button onClick="{{ openRdSalesOrder }}" style="display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:42px; border:1px solid #0a5ca8; border-radius:12px; padding:10px 14px; background:#0a5ca8; color:#fff; font:800 13px inherit; cursor:pointer; box-shadow:0 7px 18px rgba(10,92,168,.2);"><img src="assets/icons/lucide/list-ordered.svg" alt="" aria-hidden="true" width="16" height="16" style="filter:brightness(0) invert(1);">Orden de venta</button>
6540            </sc-if>
6541            <sc-if value="{{ rdShowOperations }}" hint-placeholder-val="false">
6542              <div class="rosgan-ops-toolbar">
6543                <sc-if value="{{ rdCanManageOperations }}" hint-placeholder-val="false">
6544                <button onClick="{{ openRdCommercial }}"><img src="/assets/icons/lucide/badge-dollar-sign.svg" alt="" aria-hidden="true">Comercial</button>
6545                </sc-if>
6546                <sc-if value="{{ rdCanViewStats }}" hint-placeholder-val="false">
6547                <button onClick="{{ openRdStats }}"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="">Estadísticas</button>
6548                </sc-if>
6549                <button onClick="{{ openRdPrices }}"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="" aria-hidden="true">Precios</button>
6550                <sc-if value="{{ rdCanManageOperations }}" hint-placeholder-val="false">
6551                <button onClick="{{ openRdTv }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true">TV</button>
6552                <button onClick="{{ openRdTvConfig }}"><img src="/assets/icons/lucide/settings.svg" alt="" aria-hidden="true">Config. TV</button>
6553                </sc-if>
6554              </div>
6555            </sc-if>
6556            <div class="rd-view-switch" style="display:flex; background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:4px; gap:2px;">
6557              <span onClick="{{ setGridView }}" title="Grilla" style="width:38px; height:34px; border-radius:9px; display:grid; place-items:center; cursor:pointer; font-size:15px; background:{{ gridViewBg }}; color:{{ gridViewColor }};"><i class="dc-lucide dc-lucide-grid" aria-hidden="true"></i></span>
6558              <span onClick="{{ setListView }}" title="Lista" style="width:38px; height:34px; border-radius:9px; display:grid; place-items:center; cursor:pointer; font-size:15px; background:{{ listViewBg }}; color:{{ listViewColor }};"><i class="dc-lucide dc-lucide-list" aria-hidden="true"></i></span>
6559            </div>
6560          </div>
6561          <sc-if value="{{ rdFiltersOpen }}">
6562            <section id="remate-lot-filters" class="rd-filter-panel" aria-label="Filtros de lotes">
6563              <div class="rd-filter-head">
6564                <div class="rd-filter-title"><span><img src="/assets/icons/lucide/list-filter.svg" alt=""></span><strong>Filtrar lotes</strong><sc-if value="{{ rdHasActiveFilters }}"><span class="rd-filter-count">{{ rdActiveFilterCount }}</span></sc-if></div>
6565                <button type="button" class="rd-filter-clear" onClick="{{ clearRdFilters }}"><img src="/assets/icons/lucide/x.svg" alt="">Limpiar filtros</button>
6566              </div>
6567              <div class="rd-filter-grid">
6568                <sc-for list="{{ rdFilterGroups }}" as="group">
6569                  <article class="rd-filter-group">
6570                    <header class="rd-filter-group-head"><span class="rd-filter-group-icon"><img data-dc-attr-src="{{ group.icon }}" alt=""></span><strong>{{ group.label }}</strong></header>
6571                    <div class="rd-filter-options">
6572                      <sc-for list="{{ group.options }}" as="option">
6573                        <button type="button" class="rd-filter-chip" onClick="{{ option.pick }}" aria-pressed="{{ option.active }}">
6574                          <sc-if value="{{ option.hasLogo }}"><span class="rd-filter-chip-logo" style="background:{{ option.logoBg }};"><img data-dc-attr-src="{{ option.logo }}" alt=""></span></sc-if>
6575                          <span>{{ option.label }}</span><span class="rd-filter-chip-count">{{ option.count }}</span>
6576                        </button>
6577                      </sc-for>
6578                    </div>
6579                  </article>
6580                </sc-for>
6581              </div>
6582            </section>
6583          </sc-if>
6584          <sc-if value="{{ rdLoading }}" hint-placeholder-val="{{ false }}">
6585            <section class="rd-lots-loading" role="status" aria-live="polite" aria-label="Cargando lotes">
6586              <div class="rd-lots-loading-heading"><span class="rd-lots-spinner" aria-hidden="true"></span><div><strong>Cargando lotes…</strong><span>Estamos preparando el catálogo del remate.</span></div></div>
6587              <div class="rd-lots-skeletons" aria-hidden="true"><div></div><div></div><div></div><div></div></div>
6588            </section>
6589          </sc-if>
6590          <sc-if value="{{ rdLoaded }}" hint-placeholder-val="{{ true }}">
6591          <div style="display:flex; justify-content:space-between; align-items:baseline; padding:16px 2px 14px;">
6592            <span style="font-size:13.5px; color:#65758a;"><strong style="color:#112033; font-variant-numeric:tabular-nums;">{{ rdCount }}</strong> de {{ rdTotal }} lotes</span>
6593            <sc-if value="{{ rdSourceNote }}" hint-placeholder-val="{{ false }}"><span style="font-size:12px; color:#8593a6;">{{ rdSourceNote }}</span></sc-if>
6594          </div>
6595          </sc-if>
6596
6597          <!-- GRID VIEW -->
6598          <sc-if value="{{ rdShowGrid }}" hint-placeholder-val="{{ true }}">
6599            <div class="rd-lot-grid" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(240px, 1fr)); gap:16px;">
6600              <sc-for list="{{ rdLots }}" as="l" hint-placeholder-count="6">
6601                <div class="rd-lot-card" onClick="{{ l.open }}" onMouseEnter="{{ l.hoverIn }}" onMouseLeave="{{ l.hoverOut }}" data-reveal style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); cursor:pointer; display:flex; flex-direction:column; transition:transform 240ms cubic-bezier(0.22,0.72,0.18,1), box-shadow 240ms;" style-hover="transform:translateY(-6px); box-shadow:0 26px 60px rgba(0,30,60,0.18);">
6602                  <div class="rd-lot-card-media" style="position:relative; height:160px; overflow:hidden;">
6603                    <div data-lot-img style="position:absolute; inset:0; background-color:{{ l.thumbBg }}; background-image:url('{{ l.thumb }}'); background-size:{{ l.thumbSize }}; background-repeat:no-repeat; background-position:center; transition:transform 600ms cubic-bezier(0.22,0.72,0.18,1);"></div>
6604                    <sc-if value="{{ l.thumbnailPending }}" hint-placeholder-val="false"><div role="status" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;background:linear-gradient(110deg,#eaf0f6,#dbe6f0,#eaf0f6);color:#526b83;font-size:12px;"><span class="rd-lots-spinner" aria-hidden="true"></span>Preparando miniatura…</div></sc-if>
6605                    <sc-if value="{{ l.videoPreview }}" hint-placeholder-val="false"><video data-lot-preview data-src="{{ l.videoPreview }}" muted loop playsinline preload="none" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;pointer-events:none;transition:opacity 240ms;background:#001d3d;"></video></sc-if>
6606                    <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.02) 45%, rgba(0,29,61,0.68));"></div>
6607                    <sc-if value="{{ l.showNumber }}" hint-placeholder-val="false"><span style="position:absolute; top:10px; left:10px; background:{{ rdColor }}; color:#fff; font-size:12px; font-weight:800; padding:5px 12px; border-radius:100px;">{{ l.number }}</span></sc-if>
6608                    <span style="position:absolute; top:10px; right:10px; background:rgba(255,255,255,0.92); color:#112033; font-size:10.5px; font-weight:700; padding:4px 10px; border-radius:8px;">Cert. {{ l.cert }}</span>
6609                    <div class="rd-lot-card-caption" style="position:absolute; bottom:9px; left:12px; right:58px; color:#fff;"><div style="font-size:15px; font-weight:800; line-height:1.15;">{{ l.categoria }}</div><sc-if value="{{ l.showRaza }}" hint-placeholder-val="true"><div style="font-size:11.5px; color:#c8d8ea;">{{ l.raza }}</div></sc-if></div>
6610                    <span class="rd-lot-card-brand" title="{{ l.consignataria }}" aria-label="{{ l.consignataria }}" style="position:absolute;right:10px;bottom:10px;display:grid;place-items:center;width:38px;height:38px;box-sizing:border-box;padding:5px;overflow:hidden;border:1px solid rgba(255,255,255,.5);border-radius:11px;background:{{ l.consignatariaLogoBg }};box-shadow:0 5px 16px rgba(0,29,61,.25);"><sc-if value="{{ l.hasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ l.consignatariaLogo }}" alt="{{ l.consignataria }}" style="display:block;width:26px;height:26px;max-width:26px;max-height:26px;object-fit:contain;object-position:center;"></sc-if><sc-if value="{{ l.noConsignatariaLogo }}" hint-placeholder-val="false"><strong style="color:#fff;font-size:15px;line-height:1;">{{ l.consignatariaInitial }}</strong></sc-if></span>
6611                  </div>
6612                  <div class="rd-lot-card-body" style="padding:13px 15px; display:grid; grid-template-columns:1fr 1fr; gap:9px;">
6613                    <sc-for list="{{ l.facts }}" as="fct" hint-placeholder-count="4">
6614                      <div class="rd-lot-card-fact"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ fct.label }}</div><div style="font-size:14px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ fct.value }}</div></div>
6615                    </sc-for>
6616                    <button type="button" class="rd-share-lot" onClick="{{ l.shareLot }}" disabled="{{ !l.shareReady }}" style="grid-column:1/-1;min-height:36px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid #c7d9ec;border-radius:10px;background:#fff;color:#004779;padding:7px 10px;font:700 11px inherit;cursor:pointer;"><img src="/assets/icons/lucide/share-2.svg" alt="" width="14" height="14">{{ l.shareLabel }}</button><sc-if value="{{ l.showAlert }}" hint-placeholder-val="true"><button type="button" onClick="{{ l.toggleAlert }}" disabled="{{ l.alertDisabled }}" aria-pressed="{{ l.alertAriaPressed }}" title="Te avisamos por la app y por WhatsApp, si lo tenés habilitado" style="grid-column:1/-1;min-height:37px;margin-top:3px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid {{ l.alertBorder }};border-radius:10px;background:{{ l.alertBg }};color:{{ l.alertColor }};font:800 11.5px inherit;cursor:pointer;transition:background 160ms,border-color 160ms,transform 160ms;" style-hover="transform:translateY(-1px);"><img data-dc-attr-src="{{ l.alertIcon }}" alt="" aria-hidden="true" width="15" height="15" style="display:block;width:15px;height:15px;">{{ l.alertLabel }}</button></sc-if>
6617                  </div>
6618                </div>
6619              </sc-for>
6620            </div>
6621          </sc-if>
6622
6623          <!-- LIST VIEW -->
6624          <sc-if value="{{ rdShowList }}" hint-placeholder-val="{{ false }}">
6625            <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden;">
6626              <sc-for list="{{ rdLots }}" as="l" hint-placeholder-count="6">
6627                <div class="rd-responsive-lot-row" onClick="{{ l.open }}" data-reveal style="display:flex; align-items:center; gap:14px; padding:12px 16px; border-bottom:1px solid #eef3f9; cursor:pointer; transition:background 160ms;" style-hover="background:#f6f9fc;">
6628                  <sc-if value="{{ l.showNumber }}" hint-placeholder-val="false"><span class="rd-row-number" style="width:44px; height:44px; border-radius:10px; background:{{ rdColor }}; color:#fff; display:grid; place-items:center; font-size:12px; font-weight:800; flex-shrink:0; text-align:center; line-height:1.05;">{{ l.numberShort }}</span></sc-if>
6629                  <div class="rd-row-thumb" style="width:64px; height:48px; border-radius:10px; background-color:{{ l.thumbBg }}; background-image:url('{{ l.thumb }}'); background-size:{{ l.thumbSize }}; background-repeat:no-repeat; background-position:center; flex-shrink:0;"></div>
6630                  <span class="rd-row-brand" title="{{ l.consignataria }}" aria-label="{{ l.consignataria }}" style="display:grid;place-items:center;width:34px;height:34px;box-sizing:border-box;padding:5px;border-radius:10px;background:{{ l.consignatariaLogoBg }};box-shadow:0 4px 12px rgba(0,29,61,.14);flex:0 0 34px;"><sc-if value="{{ l.hasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ l.consignatariaLogo }}" alt="{{ l.consignataria }}" style="display:block;max-width:100%;max-height:100%;object-fit:contain;"></sc-if><sc-if value="{{ l.noConsignatariaLogo }}" hint-placeholder-val="false"><strong style="color:#fff;font-size:13px;line-height:1;">{{ l.consignatariaInitial }}</strong></sc-if></span>
6631                  <div class="rd-row-copy" style="flex:1; min-width:0;"><div style="font-size:15px; font-weight:700; color:#112033;">{{ l.categoria }}</div><div style="font-size:12px; color:#65758a;">{{ l.listMeta }}</div></div>
6632                  <button type="button" class="rd-share-lot" onClick="{{ l.shareLot }}" disabled="{{ !l.shareReady }}" style="grid-column:1/-1;min-height:36px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid #c7d9ec;border-radius:10px;background:#fff;color:#004779;padding:7px 10px;font:700 11px inherit;cursor:pointer;"><img src="/assets/icons/lucide/share-2.svg" alt="" width="14" height="14">{{ l.shareLabel }}</button><sc-if value="{{ l.showAlert }}" hint-placeholder-val="true"><button class="rd-row-alert" type="button" onClick="{{ l.toggleAlert }}" disabled="{{ l.alertDisabled }}" aria-pressed="{{ l.alertAriaPressed }}" title="Te avisamos por la app y por WhatsApp, si lo tenés habilitado" style="min-height:36px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid {{ l.alertBorder }};border-radius:10px;padding:0 11px;background:{{ l.alertBg }};color:{{ l.alertColor }};font:800 11px inherit;cursor:pointer;"><img data-dc-attr-src="{{ l.alertIcon }}" alt="" aria-hidden="true" width="14" height="14" style="display:block;width:14px;height:14px;">{{ l.alertLabel }}</button></sc-if>
6633                  <sc-if value="{{ l.showCabezas }}" hint-placeholder-val="true"><div class="rd-row-heads" style="text-align:right; min-width:70px;"><div style="font-size:10px; color:#8593a6;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.cabezas }}</div></div></sc-if>
6634                  <sc-if value="{{ l.showPeso }}" hint-placeholder-val="true"><div class="rd-row-weight" style="text-align:right; min-width:80px;"><div style="font-size:10px; color:#8593a6;">PESO PROM.</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.peso }}</div></div></sc-if>
6635                </div>
6636              </sc-for>
6637            </div>
6638          </sc-if>
6639          <sc-if value="{{ rdEmpty }}" hint-placeholder-val="{{ false }}">
6640            <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;">
6641              <div style="font-size:16px; font-weight:700; color:#112033;">{{ rdEmptyTitle }}</div>
6642              <div style="font-size:13.5px; margin-top:4px;">{{ rdEmptyText }}</div>
6643            </div>
6644          </sc-if>
6645          <div style="height:40px;"></div>
6646        </div>
6647
6648        </sc-if>
6649        <!-- FICHA DE LOTE ROSGAN / HISTÓRICO -->
6650        <sc-if value="{{ rdLotPage }}" hint-placeholder-val="false">
6651          <div class="rg-lot-page" style="background:#f4f7fb;box-sizing:border-box;">
6652            <main style="width:100%; max-width:1240px; min-width:0; box-sizing:border-box; margin:0 auto; padding:26px clamp(14px,4vw,36px) 100px;">
6653              <button onClick="{{ closeLightbox }}" style="display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:#0a5ca8;font:700 13px inherit;padding:0;cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver al catálogo</button>
6654              <header style="display:flex;justify-content:space-between;align-items:flex-end;
6654gap:14px;flex-wrap:wrap;margin:10px 0 18px;">
6655                <div><div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:5px;"><sc-if value="{{ lbHasNumber }}"><span style="display:inline-flex;align-items:center;min-height:26px;padding:0 11px;border-radius:999px;background:{{ rdColor }};color:#fff;font-size:11px;font-weight:900;">{{ lbNumber }}</span></sc-if><span style="color:#718196;font-size:12px;font-weight:800;">CERT. {{ lbCert }}</span></div><h1 style="margin:0;color:#112033;font-size:clamp(26px,3.4vw,36px);line-height:1.08;">{{ lbCategoria }}</h1><div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:8px;"><span style="display:inline-flex;align-items:center;gap:6px;min-height:34px;padding:0 10px;border:1px solid #cfe0ef;border-radius:9px;background:#edf5fb;color:#174c75;font-size:12px;font-weight:800;"><img src="assets/icons/lucide/map-pin.svg" alt="" aria-hidden="true" width="13" height="13" style="display:block;width:13px;height:13px;">{{ lbLocation }}</span></div></div>
6656                <nav class="rg-lot-navigation" aria-label="Navegar entre lotes">
6657                  <button type="button" onClick="{{ shareCurrentLot }}" disabled="{{ !lbShareReady }}"><img src="/assets/icons/lucide/share-2.svg" alt="" aria-hidden="true">{{ lbShareLabel }}</button>
6658                  <sc-if value="{{ lbHasPreviousLot }}" hint-placeholder-val="false"><button type="button" onClick="{{ lbPreviousLot }}"><img src="assets/icons/lucide/arrow-left.svg" alt="" aria-hidden="true">Anterior lote</button></sc-if>
6659                  <sc-if value="{{ lbHasNextLot }}" hint-placeholder-val="false"><button type="button" onClick="{{ lbNextLot }}">Siguiente lote<img src="assets/icons/lucide/arrow-right.svg" alt="" aria-hidden="true"></button></sc-if>
6660                </nav>
6661              </header>
6662              <div style="display:grid;grid-template-columns:{{ rdLotDetailCols }};gap:20px;align-items:start;min-width:0;">
6663                <div style="display:grid;gap:12px;min-width:0;">
6664                  <div class="dc-cabana-video-frame"><sc-if value="{{ lbPrimaryVideoUrl }}" hint-placeholder-val="false"><video class="dc-cabana-lot-player" data-remate-lot-player="true" data-src="{{ lbPrimaryVideoUrl }}" data-dc-attr-src="{{ lbPrimaryVideoUrl }}" data-dc-attr-poster="{{ lbThumb }}" onLoadedMetadata="{{ lbVideoMeta }}" onTimeUpdate="{{ lbVideoTimeUpdate }}" onVolumeChange="{{ lbVideoVolumeChange }}" onPause="{{ lbVideoPause }}" onPlay="{{ lbVideoPlay }}" onPlaying="{{ lbVideoPlay }}" onEnded="{{ lbVideoEnded }}" autoplay muted loop preload="auto" playsinline style="display:block;width:100%;height:100%;object-fit:cover;background:#001d3d;"></video></sc-if><sc-if value="{{ lbNoPrimaryVideo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ lbThumb }}" alt="{{ lbCategoria }}" style="display:block;width:100%;height:100%;object-fit:cover;"></sc-if><div class="rg-video-certifications"><sc-if value="{{ lbShowVisec }}" hint-placeholder-val="false"><span class="rg-video-certification rg-video-visec" title="Apto VISEC"><img src="/assets/icons/visec-mark.svg" alt=""><span>VISEC</span></span></sc-if><sc-if value="{{ lbShowChina }}" hint-placeholder-val="false"><span class="rg-video-certification rg-video-china" title="Apto China"><img src="/assets/icons/china-flag.svg" alt=""><span>Apto China</span></span></sc-if></div><sc-if value="{{ lbHasConsignatariaLogo }}" hint-placeholder-val="false"><span class="rg-video-cons-brand" style="background:{{ lbConsignatariaColor }};" title="{{ lbConsignataria }}"><img data-dc-attr-src="{{ lbConsignatariaLogo }}" alt="{{ lbConsignataria }}"></span></sc-if></div>
6665                  <sc-if value="{{ lbPrimaryVideoUrl }}" hint-placeholder-val="false"><div class="dc-video-controls" aria-label="Controles del video"><button class="dc-video-control-button" onClick="{{ lbTogglePlay }}" title="{{ lbPlayLabel }}" aria-label="{{ lbPlayLabel }}"><img data-dc-attr-src="{{ lbPlayIcon }}" alt=""></button><input class="dc-video-slider" type="range" min="0" max="100" step="0.1" value="{{ lbVideoProgress }}" onInput="{{ lbSeekVideo }}" onChange="{{ lbSeekVideo }}" aria-label="Avance del video"><span class="dc-video-time">{{ lbVideoTime }}</span><button class="dc-video-control-button" onClick="{{ lbFullscreen }}" title="Pantalla completa" aria-label="Pantalla completa"><img src="assets/icons/lucide/maximize-2.svg" alt=""></button></div></sc-if>
6666                  <sc-for list="{{ lbDetailGroups }}" as="group" hint-placeholder-count="3"><section style="background:#fff;border:1px solid #e3eaf2;border-radius:17px;padding:18px;box-shadow:0 10px 28px rgba(0,30,60,.05);"><small style="display:block;color:#8593a6;font-weight:900;letter-spacing:.11em;margin-bottom:11px;">{{ group.label }}</small><div class="rg-detail-list"><sc-for list="{{ group.cards }}" as="data" hint-placeholder-count="4"><div class="dc-rosgan-field-card rg-detail-row"><sc-if value="{{ data.isBreed }}" hint-placeholder-val="false"><sc-if value="{{ lbHasBreedIcons }}" hint-placeholder-val="false"><span style="display:inline-flex;align-items:center;gap:2px;"><sc-for list="{{ lbBreedIcons }}" as="breedIcon" hint-placeholder-count="2"><span style="position:relative;display:grid;place-items:center;width:27px;height:29px;flex:none;"><img data-dc-attr-src="{{ breedIcon.icon }}" alt="" aria-hidden="true" width="25" height="25" style="display:block;width:25px;height:25px;object-fit:contain;"><span style="position:absolute;right:-1px;bottom:0;display:flex;gap:1px;"><sc-if value="{{ breedIcon.showBlack }}" hint-placeholder-val="false"><i aria-hidden="true" style="display:block;width:7px;height:7px;border-radius:50%;background:#17191d;border:1px solid #fff;"></i></sc-if><sc-if value="{{ breedIcon.showRed }}" hint-placeholder-val="false"><i aria-hidden="true" style="display:block;width:7px;height:7px;border-radius:50%;background:#a84735;border:1px solid #fff;"></i></sc-if></span></span></sc-for></span></sc-if></sc-if><sc-if value="{{ !data.isBreed }}"><span class="rg-detail-icon" aria-hidden="true"><i style="background-position:{{ data.iconX }}% {{ data.iconY }}%;"></i></span></sc-if><span class="dc-rosgan-field-copy"><small>{{ data.label }}</small><strong>{{ data.value }}</strong></span></div></sc-for></div></section></sc-for>
6667                  <sc-if value="{{ lbHasObservations }}" hint-placeholder-val="false"><section style="background:#fff;border:1px solid #e3eaf2;border-radius:17px;padding:18px;box-shadow:0 10px 28px rgba(0,30,60,.05);"><small class="dc-rosgan-observations-head"><span class="rg-detail-icon" aria-hidden="true"><i style="background-position:71.428571% 100%;"></i></span>OBSERVACIONES</small><p style="margin:9px 0 0;color:#3d4c5f;font-size:14px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere;">{{ lbObservations }}</p></section></sc-if>
6668                </div>
6669                <aside style="position:{{ rdLotAsidePosition }};top:88px;display:grid;gap:12px;min-width:0;width:100%;">
6670                  <section style="border-radius:18px;padding:21px;color:#fff;background:linear-gradient(145deg,#002e5f,{{ rdColor }});box-shadow:0 18px 48px rgba(0,30,60,.2);"><small style="font-weight:900;letter-spacing:.1em;opacity:.75;">{{ lbNumber }}</small><strong style="display:block;margin-top:5px;font-size:27px;line-height:1.08;">{{ lbCategoria }}</strong><span style="display:block;margin-top:7px;color:rgba(255,255,255,.76);font-size:12px;line-height:1.4;">{{ lbRaza }}</span></section>
6671                  <section style="display:flex;align-items:center;gap:12px;min-width:0;color:#fff;background:{{ lbConsignatariaColor }};border-radius:15px;padding:14px 15px;box-shadow:0 10px 26px {{ lbConsignatariaShadow }};"><span class="dc-consignataria-logo-box" style="width:42px;height:42px;flex:0 0 42px;"><sc-if value="{{ lbHasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ lbConsignatariaLogo }}" alt="{{ lbConsignataria }}"></sc-if><sc-if value="{{ lbNoConsignatariaLogo }}" hint-placeholder-val="false"><strong style="font-size:18px;">{{ lbConsignatariaInitial }}</strong></sc-if></span><span style="min-width:0;"><small style="display:block;color:rgba(255,255,255,.78);font-weight:800;letter-spacing:.07em;">CONSIGNATARIA</small><strong style="display:block;color:#fff;line-height:1.2;">{{ lbConsignataria }}</strong></span></section>
6672                  <sc-if value="{{ lbHasRepresentative }}" hint-placeholder-val="false"><section class="dc-lot-contact-card">
6673                    <div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar {{ lbRepresentativePhotoClass }}" style="background:{{ lbRepresentativeAvatarBg }};"><sc-if value="{{ lbRepresentativeHasAvatar }}"><img data-dc-attr-src="{{ lbRepresentativeAvatar }}" onError="{{ lbRepresentativeAvatarError }}" alt="" style="width:{{ lbRepresentativeAvatarSize }};height:{{ lbRepresentativeAvatarSize }};object-fit:{{ lbRepresentativeAvatarFit }};"></sc-if></span><span class="dc-lot-contact-copy"><small>REPRESENTANTE COMERCIAL</small><strong>{{ lbRepresentativeName }}</strong><sc-if value="{{ lbRepresentativeHasPhone }}"><span>{{ lbRepresentativePhone }}</span></sc-if></span></div>
6674                    <sc-if value="{{ lbRepresentativeHasContact }}"><div class="dc-lot-contact-actions"><sc-if value="{{ lbRepresentativeHasWhatsapp }}"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ lbRepresentativeWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">WhatsApp</a></sc-if><sc-if value="{{ lbRepresentativeHasPhone }}"><a class="dc-contact-call" data-dc-attr-href="{{ lbRepresentativePhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></sc-if></div></sc-if>
6675                    <sc-if value="{{ lbRepresentativeNoContact }}"><p style="margin:12px 0 0;color:#718196;font-size:12px;line-height:1.4;">Sin teléfono de contacto cargado.</p></sc-if>
6676                  </section></sc-if>
6677                  <section style="background:#fff;border:1px solid #e3eaf2;border-radius:15px;padding:15px;"><small style="display:block;color:#8593a6;font-weight:800;letter-spacing:.06em;">REMATE</small><strong style="display:block;margin-top:4px;color:#112033;line-height:1.25;">{{ rdTitle }}</strong><span style="display:block;margin-top:5px;color:#718196;font-size:11px;line-height:1.4;">{{ rdDate }}</span></section>
6678                </aside>
6679              </div>
6680            </main>
6681          </div>
6682        </sc-if>
6683
6684        <!-- LOT LIGHTBOX LEGACY -->
6685        <sc-if value="{{ rdLightbox }}" hint-placeholder-val="{{ false }}">
6686          <div onClick="{{ closeLightbox }}" style="position:fixed; inset:0; z-index:80; background:rgba(0,20,45,0.6); backdrop-filter:blur(6px); display:grid; place-items:center; padding:20px; animation:pageIn 240ms both;">
6687            <div onClick="{{ stopProp }}" style="background:#fff; border-radius:22px; overflow:hidden; max-width:760px; width:100%; max-height:90vh; overflow-y:auto; box-shadow:0 40px 100px rgba(0,20,45,0.4); animation:floatIn 320ms cubic-bezier(0.22,0.72,0.18,1) both;">
6688              <div style="position:relative; aspect-ratio:16/10;">
6689                <div style="position:absolute; inset:0; background-image:url('{{ lbThumb }}'); background-size:cover; background-position:center;"></div>
6690                <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 50%, rgba(0,29,61,0.7));"></div>
6691                <span onClick="{{ closeLightbox }}" style="position:absolute; top:12px; right:12px; width:34px; height:34px; border-radius:50%; background:rgba(0,29,61,0.7); color:#fff; display:grid; place-items:center; cursor:pointer; font-size:16px;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></span>
6692                <span style="position:absolute; top:12px; left:12px; background:{{ rdColor }}; color:#fff; font-size:12px; font-weight:800; padding:6px 14px; border-radius:100px;">{{ lbNumber }}</span>
6693                <div style="position:absolute; bottom:14px; left:16px; right:16px; color:#fff;"><div style="font-size:22px; font-weight:800; line-height:1.1;">{{ lbCategoria }}</div><div style="font-size:13px; color:#c8d8ea; margin-top:2px;">Cert. {{ lbCert }} &middot; {{ lbRaza }}</div></div>
6694              </div>
6695              <div style="padding:20px 22px;">
6696                <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(120px, 1fr)); gap:10px;">
6697                  <sc-for list="{{ lbFacts }}" as="d" hint-placeholder-count="4">
6698                    <div style="background:#f6f9fc; border-radius:12px; padding:12px 14px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.04em;">{{ d.label }}</div><div style="font-size:16px; font-weight:800; color:#112033; margin-top:2px;">{{ d.value }}</div></div>
6699                  </sc-for>
6700                </div>
6701                  <sc-if value="{{ lbCanEditRosganLot }}" hint-placeholder-val="false"><button type="button" onClick="{{ editRosganLot }}" style="margin-top:16px;border:1px solid #b8d3ea;border-radius:11px;background:#edf6ff;color:#07518f;padding:11px 15px;font:800 13px inherit;cursor:pointer;"><i class="dc-lucide dc-lucide-pencil" aria-hidden="true"></i> Editar lote cargado</button></sc-if>
6702                  <sc-if value="{{ lbRosganVideoManager }}" hint-placeholder-val="false">
6703                  <section style="margin-top:18px; padding-top:18px; border-top:1px solid #e5edf5;">
6704                    <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex-wrap:wrap;">
6705                      <div><div style="font-size:10.5px; letter-spacing:.12em; color:#0a5ca8; font-weight:850;">VIDEOS DEL LOTE</div><h3 style="font-size:18px; color:#112033; margin:3px 0 0;">{{ lbVideoTitle }}</h3><p style="font-size:12px; color:#65758a; margin:4px 0 0;">Hasta 10 videos por vez · Máximo 1 GB cada uno.</p></div>
6706                      <sc-if value="{{ lbCanManageVideos }}" hint-placeholder-val="false"><label style="display:inline-flex; align-items:center; gap:8px; border-radius:11px; padding:10px 14px; background:#0a5ca8; color:#fff; font-size:12.5px; font-weight:800; cursor:pointer; box-shadow:0 8px 18px rgba(10,92,168,.2);"><input type="file" multiple accept="video/*,.mp4,.mov,.m4v,.webm,.avi,.mpeg,.mpg" disabled="{{ lbVideoUploadBusy }}" onChange="{{ uploadRosganLotVideos }}" style="display:none;"><i class="dc-lucide dc-lucide-upload-cloud" aria-hidden="true"></i>Agregar videos</label><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a></sc-if>
6707                    </div>
6708                    <sc-if value="{{ lbVideoHasError }}" hint-placeholder-val="false"><div style="margin-top:12px; padding:10px 12px; border-radius:10px; background:#fff3f4; border:1px solid #efc6ca; color:#a32934; font-size:12.5px;">{{ lbVideoError }}</div></sc-if>
6709                    <sc-if value="{{ lbVideoHasUploads }}" hint-placeholder-val="false"><div style="display:grid; gap:8px; margin-top:13px;"><sc-for list="{{ lbVideoUploads }}" as="u" hint-placeholder-count="2"><div style="border:1px solid #dce7f2; border-radius:11px; padding:10px 12px; background:#f8fbfe;"><div style="display:flex; justify-content:space-between; gap:12px; font-size:12px;"><strong style="color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.name }}</strong><span style="color:{{ u.color }}; flex:none;">{{ u.status }}</span></div><div style="height:5px; border-radius:10px; overflow:hidden; background:#e1eaf3; margin-top:8px;"><span style="display:block; width:{{ u.progress }}%; height:100%; border-radius:inherit; background:{{ u.bar }}; transition:width .2s ease;"></span></div><div style="font-size:10.5px; color:#7b8ca1; margin-top:5px;">{{ u.size }}</div><sc-if value="{{ u.hasError }}"><div style="font-size:11.5px;color:#b42332;margin-top:7px;">{{ u.error }}</div></sc-if><sc-if value="{{ u.canRetry }}"><button type="button" onClick="{{ u.retry }}" style="margin-top:8px;border:1px solid #bbd1e8;border-radius:9px;padding:8px 11px;background:#fff;color:#0a5ca8;font-weight:700;">{{ u.retryLabel }}</button></sc-if></div></sc-for></div></sc-if>
6710                    <sc-if value="{{ lbCanManageVideos }}" hint-placeholder-val="false"><div style="margin-top:16px;"><button class="rg-filming-guide" type="button" onClick="{{ prepareFieldLot }}" style="margin:0;"><img src="/assets/icons/lucide/camera.svg" alt="" aria-hidden="true" style="width:18px;height:18px;">Modo filmación sin internet</button><p style="max-width:600px;margin:8px 0 0;color:#60758c;font-size:12px;line-height:1.5;">Preparalo con conexión antes de salir. Los videos quedan guardados en este dispositivo y se suben cuando vuelva internet.</p></div></sc-if>
6711                    <sc-if value="{{ lbVideoLoading }}" hint-placeholder-val="false"><div style="display:flex; align-items:center; justify-content:center; gap:8px; padding:22px; color:#65758a; font-size:13px;"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true"></i>Cargando videos…</div></sc-if>
6712                    <sc-if value="{{ lbVideoHasItems }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:11px; margin-top:14px;"><sc-for list="{{ lbVideos }}" as="v" hint-placeholder-count="2"><article style="border:1px solid #dce7f2; border-radius:13px; overflow:hidden; background:#fff;"><sc-if value="{{ v.hasUrl }}" hint-placeholder-val="true"><video controls preload="metadata" data-dc-attr-src="{{ v.url }}" style="width:100%; aspect-ratio:16/9; display:block; background:#061b32;"></video></sc-if><sc-if value="{{ v.noUrl }}" hint-placeholder-val="false"><div style="aspect-ratio:16/9; display:grid; place-items:center; background:#eef4fa; color:#0a5ca8;"><i class="dc-lucide dc-lucide-file-video" aria-hidden="true" style="font-size:28px;"></i></div></sc-if><div style="display:flex; align-items:center; gap:8px; padding:10px 11px;"><div style="min-width:0; flex:1;"><strong style="display:block; font-size:12px; color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.name }}</strong><span style="display:block; font-size:10.5px; color:#7b8ca1; margin-top:2px;">{{ v.size }}</span></div><sc-if value="{{ v.canDelete }}" hint-placeholder-val="false"><button onClick="{{ v.remove }}" disabled="{{ v.deleting }}" title="Eliminar video" style="width:32px; height:32px; border-radius:9px; border:1px solid #f0c8cc; background:#fff5f5; color:#ba2d3a; display:grid; place-items:center; cursor:pointer; flex:none;"><i class="dc-lucide dc-lucide-trash-2" aria-hidden="true"></i></button></sc-if></div></article></sc-for></div></sc-if>
6713                    <sc-if value="{{ lbVideoEmpty }}" hint-placeholder-val="false"><div style="margin-top:14px; border:1px dashed #cbd9e7; border-radius:13px; padding:24px; text-align:center; color:#65758a; font-size:12.5px;"><i class="dc-lucide dc-lucide-video" aria-hidden="true" style="display:block; font-size:25px; color:#0a5ca8; margin-bottom:7px;"></i>Este lote todavía no tiene videos.</div></sc-if>
6714                  </section>
6715                </sc-if>
6716              </div>
6717            </div>
6718          </div>
6719        </sc-if>
6720      </div>
6721    </sc-if>
6722
6723    <!-- CONFIGURACIÓN DE REMATE -->
6724    <sc-if value="{{ isRemateConfig }}" hint-placeholder-val="false">
6725      <main style="max-width:1080px; margin:0 auto; padding:28px clamp(14px,4vw,36px) 100px; animation:pageIn 360ms both;">
6726        <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; margin-bottom:22px;">
6727          <div><button onClick="{{ closeRemateConfig }}" style="display:inline-flex;align-items:center;gap:6px;border:0; background:transparent; color:#0a5ca8; font:700 13px inherit; padding:0; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver a Cabañas</button><div style="font-size:11px; letter-spacing:.14em; color:#0a5ca8; font-weight:800; margin-top:14px;">ROSGAN - CABAÑAS</div><h1 style="font-size:clamp(26px,3.4vw,34px); margin:5px 0 3px; color:#112033;">{{ remateConfigTitle }}</h1><p style="margin:0; color:#65758a; font-size:13.5px;">{{ remateConfigSubtitle }}</p></div>
6728          <sc-if value="{{ rcDetailsMode }}">          <div style="display:flex; gap:9px; flex-wrap:wrap; justify-content:flex-end;"><button onClick="{{ resetRemateConfig }}" disabled="{{ remateConfigBusy }}" style="border:1px solid #dce5ef; border-radius:11px; padding:11px 16px; background:#fff; color:#3d4c5f; font:700 13px inherit; cursor:pointer; opacity:{{ remateConfigActionOpacity }};">Restablecer</button><button onClick="{{ saveRemateDraft }}" disabled="{{ remateConfigBusy }}" style="border:1px solid #b9cce0; border-radius:11px; padding:11px 16px; background:#edf5fc; color:#00457f; font:800 13px inherit; cursor:pointer; opacity:{{ remateConfigActionOpacity }};">{{ remateConfigDraftLabel }}</button><button onClick="{{ publishRemateConfig }}" disabled="{{ remateConfigBusy }}" style="border:0; border-radius:11px; padding:11px 18px; background:{{ remateConfigSaveBg }}; color:#fff; font:800 13px inherit; cursor:pointer; box-shadow:0 7px 18px rgba(230,57,70,.22); opacity:{{ remateConfigActionOpacity }};">{{ remateConfigPublishLabel }}</button></div></sc-if><sc-if value="{{ rcSchemaMode }}"><button type="button" onClick="{{ rcSaveSchema }}" disabled="{{ remateConfigBusy }}" style="border:0;border-radius:11px;padding:12px 18px;background:#0a5ca8;color:white;font:800 13px inherit;cursor:pointer">Guardar formulario de lotes</button></sc-if>
6729        </div>
6730        <sc-if value="{{ rcHasSavedRemate }}"><nav aria-label="Configuración del remate" style="display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap">
6730<button type="button" onClick="{{ rcShowDetails }}" aria-pressed="{{ rcDetailsMode }}" style="padding:11px 16px;border:1px solid #cbdced;border-radius:10px;background:{{ rcDetailsTabBg }};color:{{ rcDetailsTabColor }};font:700 13px inherit;cursor:pointer">Datos del remate</button><button type="button" onClick="{{ rcShowSchema }}" aria-pressed="{{ rcSchemaMode }}" style="padding:11px 16px;border:1px solid #cbdced;border-radius:10px;background:{{ rcSchemaTabBg }};color:{{ rcSchemaTabColor }};font:700 13px inherit;cursor:pointer">Formulario de lotes</button></nav></sc-if>
6731        <sc-if value="{{ rcDetailsMode }}">
6732        <section class="cb-catalog-access-setting"><div><h2>Acceso al catálogo de lotes</h2><p>Activado: requiere iniciar sesión o registrarse para buscar y ver lotes. Desactivado: acceso público, con visitas de usuarios e invitados.</p><strong>{{ rcCatalogAccessLabel }}</strong></div><button type="button" role="switch" aria-label="Requerir registro para ver los lotes" aria-checked="{{ rcCatalogPrivate }}" onClick="{{ rcToggleCatalogPrivate }}" class="cb-catalog-access-switch" style="background:{{ rcCatalogSwitchBg }}"><span style="transform:translateX({{ rcCatalogSwitchOffset }});"></span></button></section>
6733        <sc-for list="{{ remateConfigSections }}" as="section" hint-placeholder-count="4">
6734          <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;">
6735            <div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">{{ section.step }}</div><h2 style="font-size:17px; margin:3px 0 16px; color:#112033;">{{ section.title }}</h2>
6736            <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:14px;">
6737              <sc-for list="{{ section.fields }}" as="field" hint-placeholder-count="5"><label style="display:flex; flex-direction:column; gap:6px; font-size:12px; font-weight:700; color:#3d4c5f;"><span>{{ field.label }}</span><sc-if value="{{ field.isText }}" hint-placeholder-val="true"><input value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" placeholder="{{ field.placeholder }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:11px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isDate }}" hint-placeholder-val="false"><input type="date" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isTime }}" hint-placeholder-val="false"><input type="time" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isPlace }}" hint-placeholder-val="false"><div style="position:relative;display:grid;gap:6px;"><span style="font-size:11px"><span style="display:inline-flex;gap:4px;align-items:center;font-weight:500;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;">Google</span></span><input id="rc-locality" role="combobox" aria-autocomplete="list" aria-controls="rc-place-options" aria-expanded="{{ rcPlacesExpanded }}" aria-activedescendant="{{ rcPlaceActiveId }}" value="" data-dc-attr-value="{{ rcLocality }}" onInput="{{ setRcLocality }}" onKeyDown="{{ rcPlaceKey }}" onFocus="{{ rcPlaceFocus }}" onBlur="{{ rcPlaceBlur }}" disabled="{{ rcLocalityDisabled }}" autocomplete="off" placeholder="{{ rcLocalityPlaceholder }}" style="min-width:0;width:100%;box-sizing:border-box;border:1px solid #cedbea;border-radius:12px;padding:12px 13px;font:inherit;background:#fff;"><sc-if value="{{ rcPlacesVisible }}"><div id="rc-place-options" role="listbox" aria-label="Localidades sugeridas" style="position:absolute;z-index:45;left:0;right:0;top:100%;max-height:240px;overflow:auto;padding:5px;border:1px solid #cedbea;border-radius:12px;background:#fff;box-shadow:0 12px 28px rgba(0,30,60,.16);"><sc-for list="{{ rcPlaces }}" as="place"><button type="button" role="option" id="{{ place.id }}" aria-selected="{{ place.selected }}" onPointerDown="{{ place.pickPointer }}" onClick="{{ place.pick }}" style="width:100%;padding:10px;border:0;border-radius:8px;background:{{ place.bg }};text-align:left;cursor:pointer;"><strong style="display:block;color:#123553;">{{ place.label }}</strong><small style="display:block;color:#60758c;margin-top:3px;">{{ place.address }} · {{ place.source }}</small></button></sc-for></div></sc-if><span role="status" style="font-size:11px;color:#60758c;">{{ rcPlaceStatus }}</span></div></sc-if><sc-if value="{{ field.isSelect }}" hint-placeholder-val="false"><select value="" data-dc-attr-value="{{ field.value }}" onChange="{{ field.on }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:11px 13px; outline:0; background:#fff; font:14px inherit;"><sc-for list="{{ field.options }}" as="option" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select></sc-if><sc-if value="{{ field.isSwitch }}" hint-placeholder-val="false"><button type="button" onClick="{{ field.toggle }}" style="display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%; border:1px solid #dce5ef; border-radius:11px; padding:9px 12px; background:#f8fafc; color:#3d4c5f; font:800 13px inherit; cursor:pointer;"><span style="width:44px; height:24px; border-radius:100px; background:{{ field.switchTrack }}; position:relative;"><span style="position:absolute; width:20px; height:20px; border-radius:50%; background:#fff; top:2px; left:2px; transform:translateX({{ field.switchKnob }}); transition:transform 180ms;"></span></span><span>{{ field.switchState }}</span></button></sc-if><sc-if value="{{ field.help }}" hint-placeholder-val="false"><small style="font-weight:500; color:#8593a6; line-height:1.35;">{{ field.help }}</small></sc-if></label></sc-for>
6738            </div>
6739          </section>
6740        </sc-for>
6741        <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;">
6742          <div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">05</div><h2 style="font-size:17px; margin:3px 0 4px; color:#112033;">Cabañas participantes</h2><p style="margin:0 0 18px; color:#65758a; font-size:12.5px;">El logo es opcional. Si lo cargás, debe ser PNG de hasta 2 MB.</p>
6743          <sc-for list="{{ cabanaParticipantGroups }}" as="group" hint-placeholder-count="2"><div style="margin-top:18px;"><div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px;"><div><strong style="display:block; color:#112033; font-size:14px;">{{ group.title }}</strong><small style="color:#8593a6;">{{ group.help }}</small></div><button onClick="{{ group.add }}" style="display:inline-flex; align-items:center; gap:6px; border:1px solid #cfe0f5; border-radius:9px; padding:8px 11px; background:#f3f8ff; color:#0a5ca8; font:800 12px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Agregar cabaña</button></div><div style="display:grid; gap:10px;"><sc-for list="{{ group.rows }}" as="row" hint-placeholder-count="2"><div style="display:grid; grid-template-columns:58px minmax(0,1fr) 38px; gap:10px; align-items:center; border:1px solid #e3eaf2; border-radius:13px; padding:10px; background:#fafcfe;"><div style="width:54px; height:54px; border-radius:11px; border:1px solid #dce5ef; background:#fff; display:grid; place-items:center; overflow:hidden;"><sc-if value="{{ row.hasLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ row.logo }}" alt="Logo de {{ row.name }}" style="width:100%; height:100%; object-fit:contain; padding:5px; box-sizing:border-box;"></sc-if><sc-if value="{{ row.noLogo }}" hint-placeholder-val="true"><span style="color:#9fb0c3; font-size:10px; font-weight:900;">PNG</span></sc-if></div><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:8px; min-width:0;"><input value="" data-dc-attr-value="{{ row.name }}" onInput="{{ row.onName }}" placeholder="Nombre de la cabaña" style="width:100%; border:1px solid #dce5ef; border-radius:10px; padding:10px 12px; outline:0; font:13px inherit;"><label style="display:flex; align-items:center; justify-content:center; min-height:40px; border:1px dashed #9fbad6; border-radius:10px; padding:8px 10px; color:#0a5ca8; background:#f3f8ff; font:800 11px inherit; cursor:pointer; text-align:center; overflow:hidden;"><span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ row.fileLabel }}</span><input type="file" accept="image/png,.png" onChange="{{ row.onFile }}" style="display:none;"></label></div><button onClick="{{ row.remove }}" title="Quitar cabaña" style="width:36px; height:36px; border:1px solid #f0c5ca; border-radius:9px; background:#fff; color:#c62f3d; font:900 18px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div></sc-for></div></div></sc-for>
6744        </section>
6745        <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;">
6746          <div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">06</div><h2 style="font-size:17px; margin:3px 0 3px; color:#112033;">Catálogo PDF</h2><p style="margin:0 0 14px; color:#65758a; font-size:12.5px;">Adjuntá el catálogo oficial del remate, hasta 30 MB.</p><div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap; border:1px dashed #9fbad6; border-radius:12px; padding:12px; background:#f8fbff;"><div style="width:42px; height:42px; display:grid; place-items:center; border-radius:9px; background:#e63946; color:#fff; font:900 10px inherit;">PDF</div><div style="flex:1 1 190px; min-width:0;"><strong style="display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#112033; font-size:13px;">{{ remateCatalogName }}</strong><small style="color:#8593a6;">{{ remateCatalogMeta }}</small></div><label style="border:1px solid #cfe0f5; border-radius:9px; padding:8px 11px; background:#fff; color:#0a5ca8; font:800 11px inherit; cursor:pointer;">{{ remateCatalogAction }}<input type="file" accept="application/pdf,.pdf" onChange="{{ onRemateCatalogFile }}" style="display:none;"></label><sc-if value="{{ hasRemateCatalog }}" hint-placeholder-val="false"><button onClick="{{ removeRemateCatalog }}" title="Quitar catálogo" style="width:34px; height:34px; border:1px solid #f0c5ca; border-radius:9px; background:#fff; color:#c62f3d; font:900 16px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></sc-if></div>
6747        </section>
6748        <p style="font-size:13px;color:#65758a;margin:0 0 18px">Guardá el remate para configurar sus categorías y campos desde «Formulario de lotes».</p>
6749        </sc-if><sc-if value="{{ rcSchemaMode }}">
6750        <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;">
6751          <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-bottom:17px;"><div><div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">07</div><h2 style="font-size:17px; margin:3px 0 3px; color:#112033;">Formulario</h2><p style="margin:0; color:#65758a; font-size:12.5px;">Creá los campos que se solicitarán para cada categoría del remate.</p></div><button onClick="{{ addRemateCategoryForm }}" style="display:inline-flex; align-items:center; gap:6px; border:1px solid #cfe0f5; border-radius:10px; padding:9px 12px; background:#f3f8ff; color:#0a5ca8; font:800 12px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Agregar categoría</button></div>
6752          <div style="display:grid; gap:12px;">
6753            <sc-for list="{{ remateCategoryForms }}" as="category" hint-placeholder-count="2">
6754              <article style="border:1px solid #dce5ef; border-radius:13px; background:#fff; overflow:hidden;">
6755                <div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 11px; background:#f7f9fc; border-bottom:1px solid #e5ebf2;">
6756                  <span style="width:24px; height:24px; display:grid; place-items:center; border-radius:7px; background:#0a5ca8; color:#fff; font:900 11px inherit;">{{ category.number }}</span>
6757                  <input value="" data-dc-attr-value="{{ category.category }}" onInput="{{ category.onCategory }}" placeholder="Nombre de la categoría" aria-label="Categoría {{ category.number }}" style="flex:1 1 250px; min-width:0; border:0; border-bottom:1px solid #c9d7e6; padding:6px 2px; background:transparent; outline:0; color:#112033; font:800 13px inherit;">
6758                  <span style="white-space:nowrap; color:#1f7a63; font:900 10px inherit;">{{ category.visibleLabel }}</span>
6759                  <button onClick="{{ category.remove }}" title="Quitar categoría" aria-label="Quitar categoría" style="width:28px; height:28px; border:0; border-radius:7px; background:transparent; color:#c62f3d; font:900 17px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
6760                </div>
6761                <div style="display:grid; gap:9px; padding:10px;">
6762                  <sc-for list="{{ category.forms }}" as="subform" hint-placeholder-count="2">
6763                    <section aria-label="{{ subform.dropLabel }}" style="border:1px solid #e3eaf2; border-radius:11px; background:#fbfcfe; overflow:hidden;">
6764                      <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 9px; border-bottom:1px solid #e8edf3; background:#fff;">
6765                        <button draggable="false" title="Arrastrar para ordenar subformularios" aria-label="{{ subform.dragLabel }}" style="width:27px; height:27px; border:0; border-radius:7px; background:#eef4fa; display:grid; place-items:center; cursor:grab; user-select:none; touch-action:none;"><img src="assets/icons/lucide/grip-vertical.svg" alt="" draggable="false" style="width:15px; height:15px; pointer-events:none;"></button>
6766                        <span style="color:#8593a6; font:900 9px inherit; letter-spacing:.04em;">SUB {{ subform.number }}</span>
6767                        <input value="" data-dc-attr-value="{{ subform.name }}" onInput="{{ subform.onName }}" placeholder="Nombre del subformulario" aria-label="Nombre del subformulario" style="flex:1 1 210px; min-width:0; border:0; border-bottom:1px solid #d6e0eb; padding:6px 2px; outline:0; color:#112033; font:800 12px inherit;">
6768                        <button onClick="{{ subform.toggleActive }}" title="Activar o desactivar subformulario" style="display:inline-flex; align-items:center; gap:6px; border:0; background:transparent; color:#526276; font:800 10px inherit; cursor:pointer;"><span style="width:34px; height:18px; border-radius:100px; background:{{ subform.activeTrack }}; position:relative;"><span style="position:absolute; width:14px; height:14px; border-radius:50%; background:#fff; top:2px; left:2px; transform:translateX({{ subform.activeKnob }}); transition:transform 180ms;"></span></span>{{ subform.activeLabel }}</button>
6769                        <button onClick="{{ subform.remove }}" title="Quitar subformulario" aria-label="Quitar subformulario" style="width:27px; height:27px; border:0; border-radius:7px; background:#fff4f5; color:#c62f3d; font:900 15px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
6770                      </div>
6771                      <sc-if value="{{ isDesktop }}" hint-placeholder-val="true">
6772                        <div style="max-width:100%; overflow-x:auto;">
6773                          <div style="min-width:750px;">
6774                            <div style="display:grid; grid-template-columns:28px minmax(190px,1fr) 125px 96px 120px 30px; gap:8px; align-items:center; padding:6px 9px; color:#8593a6; font:850 8px inherit; letter-spacing:.04em; text-transform:uppercase;"><span></span><span>Campo</span><span>Tipo</span><span>Tarjeta</span><span>Identificador</span><span></span></div>
6775                            <sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="3">
6776                              <div aria-label="{{ field.dropLabel }}" style="display:grid; grid-template-columns:28px minmax(190px,1fr) 125px 96px 120px 30px; gap:8px; align-items:center; padding:7px 9px; border-top:1px solid #edf1f5;">
6777                                <button draggable="false" title="Arrastrar para ordenar campos" aria-label="{{ field.dragLabel }}" style="width:27px; height:30px; border:0; border-radius:7px; background:#eef4fa; display:grid; place-items:center; cursor:grab; user-select:none; touch-action:none;"><img src="assets/icons/lucide/grip-vertical.svg" alt="" draggable="false" style="width:15px; height:15px; pointer-events:none;"></button>
6778                                <input value="" data-dc-attr-value="{{ field.name }}" onInput="{{ field.onName }}" placeholder="Nombre del campo" aria-label="Campo {{ field.number }}" style="min-width:0; border:1px solid #dbe4ee; border-radius:8px; padding:8px 9px; outline:0; font:12px inherit;">
6779                                <select value="" data-dc-attr-value="{{ field.type }}" onChange="{{ field.onType }}" aria-label="Tipo de campo {{ field.number }}" style="min-width:0; border:1px solid #dbe4ee; border-radius:8px; padding:8px 9px; background:#fff; outline:0; font:12px inherit;"><sc-for list="{{ field.typeOptions }}" as="option" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select>
6780                                <button onClick="{{ field.toggleCard }}" title="Mostrar en la tarjeta del lote" style="display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid {{ field.cardBorder }}; border-radius:8px; padding:7px 6px; background:{{ field.cardTrack }}; color:{{ field.cardText }}; font:800 10px inherit; cursor:pointer; opacity:{{ field.cardOpacity }};"><img src="assets/icons/lucide/layout-panel-top.svg" alt="" style="width:14px; height:14px; filter:{{ field.cardIconFilter }};">Tarjeta</button>
6781                                <div><button onClick="{{ field.toggleIdentifier }}" title="Usar como identificador principal de la categoría" style="display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid {{ field.identifierBorder }}; border-radius:8px; padding:7px 6px; background:{{ field.identifierTrack }}; color:{{ field.identifierText }}; font:800 10px inherit; cursor:pointer;"><img src="assets/icons/lucide/key-round.svg" alt="" style="width:14px; height:14px; filter:{{ field.identifierIconFilter }};">Principal</button><button type="button" role="checkbox" aria-checked="{{ field.characteristic }}" onClick="{{ field.toggleCharacteristic }}" style="margin-top:5px;padding:7px;border:1px solid #cadbea;border-radius:8px;background:#f2f7fc;font:11px inherit;cursor:pointer;">Característico: {{ field.characteristicLabel }}</button></div>
6782                                <button onClick="{{ field.remove }}" title="Quitar campo" aria-label="Quitar campo {{ field.number }}" style="width:28px; height:28px; border:0; border-radius:7px; background:#fff4f5; color:#c62f3d; font:900 15px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
6783                                <sc-if value="{{ field.isSelection }}" hint-placeholder-val="false"><input value="" data-dc-attr-value="{{ field.optionsText }}" onInput="{{ field.onOptions }}" placeholder="Opciones separadas por coma" aria-label="Opciones del campo {{ field.number }}" style="grid-column:1 / -1; min-width:0; border:1px solid #dbe4ee; border-radius:8px; padding:8px 9px; outline:0; font:12px inherit;"></sc-if>
6784                              </div>
6785                            </sc-for>
6786                          </div>
6787                        </div>
6788                      </sc-if>
6789                      <sc-if value="{{ isMobile }}" hint-placeholder-val="false">
6790                        <div style="display:grid; gap:8px; padding:8px;">
6791                          <sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="3">
6792                            <article aria-label="{{ field.dropLabel }}" style="min-width:0; display:grid; grid-template-columns:32px minmax(0,1fr) 30px; gap:8px; align-items:start; border:1px solid #e4ebf3; border-radius:10px; padding:9px; background:#fff;">
6793                              <button draggable="false" title="Arrastrar para ordenar campos" aria-label="{{ field.dragLabel }}" style="grid-row:1 / span 3; width:30px; height:100%; min-height:72px; border:0; border-radius:8px; background:#eef4fa; display:grid; place-items:center; cursor:grab; user-select:none; touch-action:none;"><img src="assets/icons/lucide/grip-vertical.svg" alt="" draggable="false" style="width:16px; height:16px; pointer-events:none;"></button>
6794                              <input value="" data-dc-attr-value="{{ field.name }}" onInput="{{ field.onName }}" placeholder="Nombre del campo" aria-label="Campo {{ field.number }}" style="width:100%; min-width:0; box-sizing:border-box; border:1px solid #dbe4ee; border-radius:8px; padding:9px; outline:0; font:12px inherit;">
6795                              <button onClick="{{ field.remove }}" title="Quitar campo" aria-label="Quitar campo {{ field.number }}" style="width:30px; height:34px; border:0; border-radius:8px; background:#fff4f5; color:#c62f3d; font:900 15px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button>
6796                              <select value="" data-dc-attr-value="{{ field.type }}" onChange="{{ field.onType }}" aria-label="Tipo de campo {{ field.number }}" style="grid-column:2 / -1; width:100%; min-width:0; box-sizing:border-box; border:1px solid #dbe4ee; border-radius:8px; padding:9px; background:#fff; outline:0; font:12px inherit;"><sc-for list="{{ field.typeOptions }}" as="option" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select>
6797                              <div style="grid-column:2 / -1; min-width:0; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px;">
6798                                <button onClick="{{ field.toggleCard }}" title="Mostrar en la tarjeta del lote" style="min-width:0; display:inline-flex; align-items:center; justify-content:center; gap:5px; border:1px solid {{ field.cardBorder }}; border-radius:8px; padding:8px 5px; background:{{ field.cardTrack }}; color:{{ field.cardText }}; font:800 10px inherit; cursor:pointer; opacity:{{ field.cardOpacity }};"><img src="assets/icons/lucide/layout-panel-top.svg" alt="" style="width:14px; height:14px; filter:{{ field.cardIconFilter }};">Tarjeta</button>
6799                                <div><button onClick="{{ field.toggleIdentifier }}" title="Usar como identificador principal de la categoría" style="min-width:0; display:inline-flex; align-items:center; justify-content:center; gap:5px; border:1px solid {{ field.identifierBorder }}; border-radius:8px; padding:8px 5px; background:{{ field.identifierTrack }}; color:{{ field.identifierText }}; font:800 10px inherit; cursor:pointer;"><img src="assets/icons/lucide/key-round.svg" alt="" style="width:14px; height:14px; filter:{{ field.identifierIconFilter }};">Principal</button><button type="button" role="checkbox" aria-checked="{{ field.characteristic }}" onClick="{{ field.toggleCharacteristic }}" style="margin-top:5px;padding:7px;border:1px solid #cadbea;border-radius:8px;background:#f2f7fc;font:11px inherit;cursor:pointer;">Característico: {{ field.characteristicLabel }}</button></div>
6800                              </div>
6801                              <sc-if value="{{ field.isSelection }}" hint-placeholder-val="false"><input value="" data-dc-attr-value="{{ field.optionsText }}" onInput="{{ field.onOptions }}" placeholder="Opciones separadas por coma" aria-label="Opciones del campo {{ field.number }}" style="grid-column:2 / -1; width:100%; min-width:0; box-sizing:border-box; border:1px solid #dbe4ee; border-radius:8px; padding:9px; outline:0; font:12px inherit;"></sc-if>
6802                            </article>
6803                          </sc-for>
6804                        </div>
6805                      </sc-if>
6806                      <button onClick="{{ subform.addField }}" style="margin:7px 9px 9px; border:0; border-radius:8px; padding:7px 9px; background:#eef5fc; color:#0a5ca8; font:800 11px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Campo</button>
6807                    </section>
6808                  </sc-for>
6809                  <button onClick="{{ category.addSubform }}" style="justify-self:start; border:1px dashed #9fbad6; border-radius:8px; padding:7px 10px; background:#f8fbff; color:#0a5ca8; font:800 11px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Agregar subformulario</button>
6810                </div>
6811              </article>
6812            </sc-for>
6813          </div>
6814        </section>
6815        </sc-if><sc-if value="{{ rcDetailsMode }}">
6816        <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;"><div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">08</div><h2 style="font-size:17px; margin:3px 0 16px; color:#112033;">Opciones de visualización y carga</h2><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(245px,1fr)); gap:10px;"><sc-for list="{{ remateConfigOptions }}" as="option" hint-placeholder-count="6"><button onClick="{{ option.toggle }}" style="display:flex; align-items:center; gap:12px; text-align:left; border:1px solid {{ option.border }}; border-radius:12px; padding:12px 14px; background:{{ option.bg }}; color:#112033; cursor:pointer; font:700 13px inherit;"><span style="width:38px; height:22px; border-radius:100px; background:{{ option.track }}; position:relative; flex:none;"><span style="position:absolute; width:18px; height:18px; border-radius:50%; background:#fff; top:2px; left:2px; transform:translateX({{ option.knob }}); transition:transform 180ms;"></span></span>{{ option.label }}</button></sc-for></div></section>
6817        <section class="cb-contract-section"><h2>Importes y aprobadores de preofertas</h2><p>Los importes por categoría son opcionales. Podés usar las bases e incrementos individuales de cada lote. Para definir importes por categoría, guardá primero las categorías nuevas.</p>
6818          <sc-for list="{{ rcPricing }}" as="price"><div class="cb-contract-grid"><strong>{{ price.label }}</strong><label>Base<input type="number" min="0" step="0.01" disabled="{{ price.disabled }}" data-dc-attr-value="{{ price.base }}" onInput="{{ price.onBase }}"></label><label>Incremento<input type="number" min="0.01" step="0.01" disabled="{{ price.disabled }}" data-dc-attr-value="{{ price.increment }}" onInput="{{ price.onIncrement }}"></label></div></sc-for>
6819          <fieldset disabled="{{ rcApproversDisabled }}"><legend>
6819Usuarios aprobadores</legend><p>Con aprobación automática no es necesario seleccionar aprobadores.</p><p role="status">{{ rcApproversError }}</p><div class="cb-contract-options"><sc-for list="{{ rcApprovers }}" as="user"><label><input type="checkbox" checked="{{ user.checked }}" onChange="{{ user.toggle }}">{{ user.label }}</label></sc-for></div></fieldset>
6820        </section>
6821        <section style="background:linear-gradient(180deg,#f3f8ff,#fff); border:1px solid #cfe0f5; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(10,92,168,.07);"><div style="display:flex; justify-content:space-between; align-items:center; gap:15px; flex-wrap:wrap;"><div><div style="font-size:10.5px; letter-spacing:.13em; color:#0a5ca8; font-weight:900;">PREOFERTA</div><h2 style="font-size:17px; margin:3px 0 2px; color:#112033;">Configuración de preoferta</h2><p style="margin:0; color:#65758a; font-size:12.5px;">Reglas y ventana para ofertas anticipadas.</p></div><button onClick="{{ togglePreoffer }}" style="display:flex; align-items:center; gap:9px; border:1px solid {{ preofferBorder }}; border-radius:100px; padding:9px 13px; background:{{ preofferBg }}; color:{{ preofferColor }}; font:800 12px inherit; cursor:pointer;"><span style="width:8px; height:8px; border-radius:50%; background:{{ preofferDot }};"></span>{{ preofferLabel }}</button></div><sc-if value="{{ preofferEnabled }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:14px; margin-top:18px;"><sc-for list="{{ preofferFields }}" as="field" hint-placeholder-count="5"><label style="display:flex; flex-direction:column; gap:6px; font-size:12px; font-weight:700; color:#3d4c5f;"><span>{{ field.label }}</span><sc-if value="{{ field.isDate }}" hint-placeholder-val="true"><input type="date" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #cfe0f5; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isTime }}" hint-placeholder-val="false"><input type="time" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #cfe0f5; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isSelect }}" hint-placeholder-val="false"><select value="" data-dc-attr-value="{{ field.value }}" onChange="{{ field.on }}" style="width:100%; border:1px solid #cfe0f5; border-radius:11px; padding:11px 13px; outline:0; background:#fff; font:14px inherit;"><sc-for list="{{ field.options }}" as="option" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select></sc-if></label></sc-for></div></sc-if></section>
6822        </sc-if>
6823      </main>
6824    </sc-if>
6825
6826    <!-- REMATES DE CABAÑA -->
6827    <sc-if value="{{ isCabanas }}" hint-placeholder-val="false">
6828      <main style="max-width:1240px; margin:0 auto; padding:28px clamp(14px,4vw,36px) 100px; animation:pageIn 360ms both;">
6829        <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin-bottom:20px;">
6830          <div><div style="font-size:11px; letter-spacing:0.14em; color:#0a5ca8; font-weight:800;">ROSGAN - REMATES DE CABAÑA</div><h1 style="font-size:clamp(27px,3.4vw,38px); margin:5px 0 4px; color:#112033;">Remates de cabaña</h1><p style="margin:0; color:#65758a; font-size:14px; max-width:650px;">Remates de cabaña creados y administrados desde Rosgan.</p></div>
6831          <div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap;"><sc-if value="{{ showCreateRemate }}" hint-placeholder-val="false"><button onClick="{{ createCabanaRemate }}" style="display:inline-flex; align-items:center; gap:7px; border:0; border-radius:11px; padding:11px 16px; background:#e63946; color:#fff; font:800 13px inherit; cursor:pointer; box-shadow:0 7px 18px rgba(230,57,70,.22);"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>Crear remate</button></sc-if></div>
6832        </div>
6833        <sc-if value="{{ cabanasLoading }}" hint-placeholder-val="false">
6834          <div role="status" aria-live="polite" style="display:flex; align-items:center; gap:10px; margin:2px 0 17px; color:#52657b; font-size:13px; font-weight:800;">
6835            <span style="width:18px; height:18px; border:3px solid #d7e4f0; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>
6836            Cargando remates de cabaña…
6837          </div>
6838          <div class="cabana-auction-list" aria-label="Cargando remates"><sc-for list="{{ cabanaSkeletonCards }}" as="skeleton" hint-placeholder-count="6"><div class="cabana-list-skeleton"></div></sc-for></div>
6839        </sc-if>
6840        <sc-if value="{{ cabanasHasError }}" hint-placeholder-val="false"><div style="padding:34px; border:1px solid #f0c5ca; border-radius:16px; background:#fff; color:#9d2732; text-align:center;">{{ cabanasError }}</div></sc-if>
6841        <sc-if value="{{ hasCabanas }}" hint-placeholder-val="true">
6842          <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:20px;"><sc-for list="{{ cabanaBreedChips }}" as="chip" hint-placeholder-count="5"><button onClick="{{ chip.go }}" style="border:1px solid #dce5ef; background:{{ chip.bg }}; color:{{ chip.color }}; border-radius:100px; padding:9px 15px; font:700 13px inherit; cursor:pointer;">{{ chip.label }} <span style="opacity:.7; margin-left:4px;">{{ chip.count }}</span></button></sc-for></div>
6843          <div class="cabana-auction-list" aria-label="Remates de cabaña ordenados por fecha">
6844            <sc-for list="{{ cabanaCards }}" as="card" hint-placeholder-count="6">
6845              <article class="cabana-list-row" onClick="{{ card.openRow }}">
6846                <button class="cabana-list-thumb {{ card.listThumbClass }}" onClick="{{ card.open }}" aria-label="Abrir remate {{ card.title }}"><img data-dc-attr-src="{{ card.listThumb }}" alt="" loading="lazy" onError="{{ card.listThumbError }}"></button>
6847                <div class="cabana-list-identity"><span class="cabana-list-status" style="background:{{ card.badgeBg }};">{{ card.badge }}</span><sc-if value="{{ card.prebidsActive }}" hint-placeholder-val="false"><span class="prebid-active-badge"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas</span></sc-if><button class="cabana-list-title" onClick="{{ card.open }}">{{ card.title }}</button><sc-if value="{{ card.hasLogos }}"><div class="cabana-list-participants {{ card.participantRollClass }}"><div class="cabana-list-participant-window" tabindex="0" aria-label="Participantes"><div class="cabana-list-participant-track" style="animation-duration:{{ card.participantRollDuration }};"><span>{{ card.participantNames }}</span><sc-if value="{{ card.participantRoll }}"><span aria-hidden="true">{{ card.participantNames }}</span></sc-if></div></div><sc-if value="{{ card.participantRoll }}"><button class="cabana-list-roll-toggle" onClick="{{ card.toggleListRoll }}" aria-label="Pausar desplazamiento de participantes" aria-pressed="false">Ⅱ</button></sc-if></div></sc-if></div>
6848                <div class="cabana-list-date"><img src="assets/icons/lucide/calendar-days.svg" alt=""><div><strong>{{ card.dateTime }}</strong><small>{{ card.location }}</small></div></div>
6849                <div class="cabana-list-consignor"><span style="background:{{ card.consBg }};"><sc-if value="{{ card.hasConsIcon }}"><img data-dc-attr-src="{{ card.consIcon }}" alt="" style="filter:{{ card.consIconFilter }};"></sc-if></span><div><small>CONSIGNATARIA</small><strong>{{ card.consignataria }}</strong></div></div>
6850                <div class="cabana-list-actions">
6851                  <sc-if value="{{ card.canEdit }}"><button onClick="{{ card.edit }}" title="Editar remate" aria-label="Editar {{ card.title }}"><img src="assets/icons/lucide/pencil.svg" alt=""></button></sc-if>
6852                  <sc-if value="{{ card.canManageSalesOrder }}"><button onClick="{{ card.salesOrder }}" title="Orden de ventas" aria-label="Ventas de {{ card.title }}"><img src="assets/icons/lucide/list-ordered.svg" alt=""></button></sc-if>
6853                  <sc-if value="{{ card.canCreateLot }}"><button class="cabana-list-create-lot" onClick="{{ card.createLot }}" title="Cargar lote en este remate" aria-label="Cargar lote en {{ card.title }}"><img src="assets/icons/lucide/plus.svg" alt=""><span>Cargar lote</span></button></sc-if>
6854                </div>
6855              </article>
6856            </sc-for>
6857          </div>        </sc-if>
6858        <sc-if value="{{ cabanasEmpty }}" hint-placeholder-val="false"><div style="padding:46px 24px; border:1px dashed #cbd8e7; border-radius:18px; background:#fff; text-align:center;"><strong style="display:block; color:#112033;">Todavía no creaste remates de cabaña</strong><span style="font-size:13px; color:#65758a;">Los remates que agregues desde Rosgan aparecerán acá.</span></div></sc-if>
6859      </main>
6860    </sc-if>
6861
6862    <!-- CREAR / EDITAR LOTE DE CABAÑA -->
6863    <sc-if value="{{ isCabanaLotCreate }}" hint-placeholder-val="false">
6864      <main style="max-width:1120px; margin:0 auto; padding:28px clamp(14px,4vw,36px) 110px; animation:pageIn 320ms both;">
6865        <button onClick="{{ closeCabanaLotCreate }}" style="display:inline-flex;align-items:center;gap:6px;border:0; background:transparent; color:#0a5ca8; font:800 13px inherit; padding:0; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> {{ cabanaLotBackLabel }}</button>
6866        <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin:14px 0 20px;"><div><div style="font-size:10.5px; letter-spacing:.14em; color:#0a5ca8; font-weight:900;">ROSGAN - CABAÑAS</div><h1 style="margin:5px 0 4px; color:#112033; font-size:clamp(27px,3.5vw,38px);">{{ cabanaLotPageTitle }}</h1><p style="margin:0; color:#65758a; font-size:14px;">{{ cabanaLotRemateTitle }}</p></div><span style="padding:7px 12px; border-radius:100px; background:#e8f2fc; color:#0a5ca8; font-size:11px; font-weight:900;">{{ cabanaLotModeLabel }}</span></div>
6867        <sc-if value="{{ cabanaLotCreateLoading }}"><section style="min-height:220px; display:grid; place-items:center; gap:10px; border:1px solid #e3eaf2; border-radius:17px; background:#fff; color:#65758a;"><span class="dc-spinner"></span><strong>Cargando lote...</strong></section></sc-if>
6868        <sc-if value="{{ cabanaLotNoSchema }}" hint-placeholder-val="false"><section style="padding:38px 24px; border:1px dashed #b8cadc; border-radius:17px; background:#fff; text-align:center;"><strong style="display:block; color:#112033;">Este remate todavía no tiene formularios configurados</strong><span style="display:block; margin-top:5px; color:#65758a; font-size:13px;">Agregá categorías y subformularios desde el creador del remate de cabaña.</span></section></sc-if>
6869        <sc-if value="{{ cabanaLotHasSchema }}" hint-placeholder-val="true">
6870          <div style="display:grid; grid-template-columns:{{ cabanaLotLayout }}; gap:18px; align-items:start;">
6871            <div style="display:grid; gap:15px;">
6872              <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 8px 26px rgba(0,30,60,.06);"><small style="color:#8593a6; font-weight:900; letter-spacing:.12em;">CABAÑA</small><h2 style="margin:3px 0 5px; font-size:18px; color:#112033;">¿A qué cabaña pertenece?</h2><p style="margin:0 0 14px; color:#65758a; font-size:12px;">Elegí una de las cabañas participantes de este remate.</p><sc-if value="{{ cabanaLotHasCabanas }}" hint-placeholder-val="true"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:9px;"><sc-for list="{{ cabanaLotCabanas }}" as="cabana" hint-placeholder-count="3"><button onClick="{{ cabana.select }}" type="button" style="min-width:0; display:grid; grid-template-columns:52px minmax(0,1fr) 20px; gap:10px; align-items:center; text-align:left; border:1px solid {{ cabana.border }}; border-radius:12px; padding:10px; background:{{ cabana.bg }}; box-shadow:{{ cabana.shadow }}; cursor:pointer;"><span style="width:52px; height:44px; display:grid; place-items:center; border-radius:9px; background:#fff; overflow:hidden;"><sc-if value="{{ cabana.hasLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cabana.logo }}" alt="{{ cabana.name }}" style="display:block; max-width:44px; max-height:36px; object-fit:contain;"></sc-if><sc-if value="{{ cabana.noLogo }}" hint-placeholder-val="false"><b style="color:{{ cabana.brand }}; font-size:15px;">{{ cabana.initial }}</b></sc-if></span><span style="min-width:0;"><b style="display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#112033; font-size:12px;">{{ cabana.name }}</b><em style="display:block; margin-top:3px; color:{{ cabana.brand }}; font-style:normal; font-size:9px; font-weight:900; letter-spacing:.08em; text-transform:uppercase;">{{ cabana.typeLabel }}</em></span><span style="width:19px; height:19px; display:grid; place-items:center; border-radius:50%; border:2px solid {{ cabana.checkBorder }}; background:{{ cabana.checkBg }}; color:#fff; font-size:11px; font-weight:900;">{{ cabana.check }}</span></button></sc-for></div></sc-if><sc-if value="{{ cabanaLotNoCabanas }}" hint-placeholder-val="false"><div style="padding:12px; border:1px solid #f1c5c9; border-radius:10px; background:#fff4f5; color:#a72c37; font-size:12px; font-weight:800;">Este remate no tiene cabañas participantes cargadas.</div></sc-if></section>
6873              <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 8px 26px rgba(0,30,60,.06);"><small style="color:#8593a6; font-weight:900; letter-spacing:.12em;">CATEGORÍA</small><h2 style="margin:3px 0 13px; font-size:18px; color:#112033;">Tipo de lote</h2><div style="display:flex; gap:8px; flex-wrap:wrap;"><sc-for list="{{ cabanaLotCategories }}" as="category" hint-placeholder-count="3"><button onClick="{{ category.select }}" style="border:1px solid {{ category.border }}; border-radius:100px; padding:9px 14px; background:{{ category.bg }}; color:{{ category.color }}; font:800 12px inherit; cursor:pointer;">{{ category.label }}</button></sc-for></div></section>
6874              <sc-for list="{{ cabanaLotSubforms }}" as="subform" hint-placeholder-count="3"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 8px 26px rgba(0,30,60,.05);"><div style="display:flex; align-items:center; gap:9px; margin-bottom:15px;"><span style="width:28px; height:28px; display:grid; place-items:center; border-radius:8px; background:#e8f2fc; color:#0a5ca8; font-size:11px; font-weight:900;">{{ subform.number }}</span><h2 style="margin:0; color:#112033; font-size:17px;">{{ subform.name }}</h2></div><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:13px;"><sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="4"><label style="display:flex; flex-direction:column; gap:6px; color:#3d4c5f; font-size:12px; font-weight:800;"><span>{{ field.label }} <sc-if value="{{ field.isPrimary }}" hint-placeholder-val="false"><em style="font-style:normal; padding:3px 6px; border-radius:100px; background:#fff0d6; color:#9a6500; font-size:9px;">IDENTIFICADOR</em></sc-if></span><sc-if value="{{ field.isText }}" hint-placeholder-val="true"><input value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" placeholder="{{ field.placeholder }}" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isNumber }}" hint-placeholder-val="false"><input type="number" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" placeholder="0" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isDate }}" hint-placeholder-val="false"><input type="date" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:10px 12px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isSelect }}" hint-placeholder-val="false"><select value="" data-dc-attr-value="{{ field.value }}" onChange="{{ field.on }}" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; background:#fff; font:14px inherit;"><option value="">Seleccionar</option><sc-for list="{{ field.options }}" as="option" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ option }}">{{ option }}</option></sc-for></select></sc-if><sc-if value="{{ field.isBoolean }}" hint-placeholder-val="false"><button onClick="{{ field.toggle }}" type="button" style="display:flex; align-items:center; gap:9px; width:max-content; border:1px solid #d6e1ed; border-radius:100px; padding:7px 11px; background:#f8fafc; color:#3d4c5f; font:800 12px inherit; cursor:pointer;"><span style="width:34px; height:19px; border-radius:100px; background:{{ field.track }}; position:relative;"><span style="position:absolute; top:2px; left:2px; width:15px; height:15px; border-radius:50%; background:#fff; transform:translateX({{ field.knob }});"></span></span>{{ field.booleanLabel }}</button></sc-if></label></sc-for></div></section></sc-for>
6875              <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><label style="display:flex; flex-direction:column; gap:6px; color:#3d4c5f; font-size:12px; font-weight:800;">Observaciones<textarea value="" data-dc-attr-value="{{ cabanaLotObservations }}" onInput="{{ onCabanaLotObservations }}" rows="4" placeholder="Información adicional del lote" style="resize:vertical; width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; font:14px inherit;"></textarea></label></section>
6876            </div>
6877            <aside style="position:{{ cabanaLotMediaPosition }}; top:76px; display:grid; gap:15px;">
6878              <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:17px;"><div style="display:flex; justify-content:space-between; align-items:center;"><strong>Imágenes</strong><span style="font-size:11px; color:#8593a6;">{{ cabanaLotImageCount }}/8</span></div><label style="display:grid; place-items:center; gap:7px; min-height:88px; margin-top:12px; border:1px dashed #9fbad6; border-radius:12px; background:#f5f9fd; color:#0a5ca8; cursor:pointer;"><img src="assets/icons/lucide/image-plus.svg" alt="" style="width:22px; height:22px;"><b style="font-size:12px;">Agregar imágenes</b><input type="file" accept="image/jpeg,image/png,image/webp" multiple onChange="{{ onCabanaLotImages }}" style="display:none;"></label><sc-if value="{{ cabanaLotHasImages }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px; margin-top:10px;"><sc-for list="{{ cabanaLotImages }}" as="media" hint-placeholder-count="3"><figure style="position:relative; margin:0; aspect-ratio:1; border-radius:9px; overflow:hidden; background:#edf2f7;"><img data-dc-attr-src="{{ media.url }}" alt="{{ media.name }}" style="width:100%; height:100%; object-fit:cover;"><button onClick="{{ media.remove }}" title="Quitar imagen" style="position:absolute; top:4px; right:4px; width:24px; height:24px; border:0; border-radius:50%; background:rgba(0,29,61,.82); color:#fff; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></figure></sc-for></div></sc-if></section>
6879              <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:17px;"><div style="display:flex; justify-content:space-between; align-items:center;"><strong>Videos del lote</strong><span style="font-size:11px; color:#8593a6;">{{ cabanaLotVideoCount }}
6879/3</span></div><label style="display:grid; place-items:center; gap:7px; min-height:88px; margin-top:12px; border:1px dashed #9fbad6; border-radius:12px; background:#f5f9fd; color:#0a5ca8; cursor:pointer;"><img src="assets/icons/lucide/video.svg" alt="" style="width:22px; height:22px;"><b style="font-size:12px;">Agregar videos</b><input type="file" accept="video/*" multiple onChange="{{ onCabanaLotVideos }}" style="display:none;"></label><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a><sc-if value="{{ cabanaLotHasVideos }}" hint-placeholder-val="false"><div style="display:grid; gap:8px; margin-top:10px;"><sc-for list="{{ cabanaLotVideos }}" as="media" hint-placeholder-count="2"><div style="display:grid; grid-template-columns:90px minmax(0,1fr) 28px; gap:8px; align-items:center; padding:6px; border:1px solid #e3eaf2; border-radius:10px;"><video data-dc-attr-src="{{ media.url }}" muted preload="metadata" style="width:90px; aspect-ratio:16/9; object-fit:cover; border-radius:7px; background:#001d3d;"></video><span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; font-weight:800;">{{ media.name }}</span><button onClick="{{ media.remove }}" title="Quitar video" style="width:26px; height:26px; border:0; border-radius:50%; background:#fff0f1; color:#c62f3d; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div></sc-for></div></sc-if></section>
6880              <button onClick="{{ saveCabanaLotDraft }}" disabled="{{ cabanaLotCreateSaving }}" style="width:100%; border:0; border-radius:11px; padding:12px 16px; background:#e63946; color:#fff; font:900 13px inherit; cursor:{{ cabanaLotSaveCursor }}; opacity:{{ cabanaLotSaveOpacity }};">{{ cabanaLotSaveLabel }}</button>
6881            </aside>
6882          </div>
6883        </sc-if>
6884      </main>
6885    </sc-if>
6886
6887    <!-- DETALLE DE REMATE DE CABAÑA -->
6888    <sc-if value="{{ isCabanaPublicLoading }}" hint-placeholder-val="false">
6889      <main style="min-height:100vh; display:grid; place-items:center; background:#f4f7fb; padding:28px;">
6890        <section style="width:min(360px,100%); display:grid; gap:14px; justify-items:center; text-align:center; color:#65758a;">
6891          <img src="/assets/rosgan-logo-blue.png" alt="Rosgan" style="width:150px; max-width:56vw; height:auto;">
6892          <span aria-label="Preparando remate" style="width:30px; height:30px; border-radius:50%; border:3px solid #dbe6f1; border-top-color:#0a5ca8; animation:spin 700ms linear infinite;"></span>
6893          <strong style="color:#112033; font-size:16px;">Preparando remate</strong>
6894        </section>
6895      </main>
6896    </sc-if>
6897          <sc-if value="{{ cbAuditorOpen }}"><main class="ra-panel cb-auditor cb-auditor-page" aria-label="Panel auditor de cabañas">
6898            <header class="ra-top"><button class="ra-back" onClick="{{ closeCbAuditor }}" aria-label="Volver al remate">←</button><div><div class="ra-brand">ROSGAN · GESTIÓN DE CABAÑAS</div><h1>Panel Auditor</h1><p>{{ cbAuditorName }}</p></div><span class="ra-private"><i></i>Acceso interno</span></header>
6899            <div class="ra-content"><div class="ra-summary"><span><strong>{{ cbAuditorCount }}</strong> lotes</span><span>Bases, incrementos y videos</span></div>
6900            <div class="ra-toolbar"><label class="ra-search"><input aria-label="Buscar lotes del auditor" placeholder="Buscar lote, RP, nombre o categoría…" data-dc-attr-value="{{ cbAuditorQuery }}" onInput="{{ searchCbAuditor }}"></label><button class="ra-button" onClick="{{ refreshCbAuditor }}">↻ Actualizar</button></div>
6901            <p style="margin:0;font-size:12px;">Dejá un importe vacío para heredar la categoría. Guardá cada fila al terminar.</p>
6902            <sc-if value="{{ cbAuditorLoading }}"><p role="status">Cargando lotes…</p></sc-if><sc-if value="{{ cbAuditorError }}"><p class="ra-alert" role="alert">{{ cbAuditorError }}</p></sc-if>
6903            <div class="ra-list"><div class="cb-auditor-columns"><span>LOTE / RP / CATEGORÍA</span><span>BASE E INCREMENTO</span><span>EDICIÓN Y VIDEOS</span></div>
6904            <sc-for list="{{ cbAuditorRows }}" as="row"><article class="cb-auditor-row">
6905              <div><strong>{{ row.title }}</strong><small>{{ row.subtitle }}</small><small>{{ row.effective }}</small></div>
6906              <form class="cb-auditor-pricing" onSubmit="{{ row.save }}"><label>Base ($)<input type="number" name="base" disabled="{{ row.saving }}" min="0" step="0.01" placeholder="Categoría" aria-label="Base del lote" data-dc-attr-value="{{ row.base }}" onInput="{{ row.setBase }}"></label><label>Incremento ($)<input type="number" name="increment" disabled="{{ row.saving }}" min="0.01" step="0.01" placeholder="Categoría" aria-label="Incremento del lote" data-dc-attr-value="{{ row.increment }}" onInput="{{ row.setIncrement }}"></label><button type="submit" disabled="{{ row.saving }}">{{ row.saveLabel }}</button></form>
6907              <div class="cb-auditor-action-cell"><div class="cb-auditor-actions"><button class="cb-auditor-edit" onClick="{{ row.edit }}" aria-label="Editar lote" title="Editar lote"><img src="assets/icons/lucide/pencil.svg" alt=""></button><sc-if value="{{ row.canUpload }}"><label class="cb-auditor-upload"><img src="assets/icons/lucide/upload.svg" alt="">Crudos<input type="file" accept="video/*" multiple data-no-filming-assistant="true" data-video-type="raw" aria-label="Subir videos crudos" onChange="{{ row.upload }}" disabled="{{ row.uploading }}"></label><sc-if value="{{ row.canFinal }}"><label class="cb-auditor-upload"><img src="assets/icons/lucide/upload.svg" alt="">Final<input type="file" accept="video/*" data-no-filming-assistant="true" data-video-type="final" aria-label="Subir un video final" onChange="{{ row.upload }}" disabled="{{ row.uploading }}"></label></sc-if></sc-if></div><small role="status">{{ row.status }}</small></div>
6908            </article></sc-for><sc-if value="{{ cbAuditorEmpty }}"><p>No hay lotes que coincidan con la búsqueda.</p></sc-if>
6909          </div></div></main></sc-if>
6910
6911    <sc-if value="{{ isCabanaDetail }}" hint-placeholder-val="false">
6912      <main style="min-height:100vh; background:#f4f7fb;">
6913        <section class="cb-detail-hero" style="--cb-hero-color:{{ cbColor }}; --cb-hero-text:{{ cbHeroText }}; --cb-hero-muted:{{ cbHeroMuted }}; --cb-hero-surface:{{ cbHeroSurface }}; --cb-hero-surface-strong:{{ cbHeroSurfaceStrong }}; --cb-hero-border:{{ cbHeroBorder }}; --cb-hero-icon-filter:{{ cbHeroIconFilter }};">
6914          <div class="cb-detail-hero-shell">
6915            <button class="cb-detail-back" onClick="{{ goCabanas }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver a remates de cabaña</button>
6916            <div class="cb-detail-hero-main">
6917              <div class="cb-detail-copy">
6918                <span class="cb-detail-breed">{{ cbBreed }}</span>
6919                <h1 class="cb-detail-title">{{ cbTitle }}</h1>
6920                <div class="cb-detail-meta">
6921                  <span class="cb-detail-meta-item"><img src="assets/icons/lucide/calendar-days.svg" alt="">{{ cbDate }} · {{ cbTime }}</span>
6922                  <span class="cb-detail-meta-item"><img src="assets/icons/lucide/map-pin.svg" alt="">{{ cbLocation }}</span>
6923                </div>
6924              </div>
6925              <sc-if value="{{ cbHasBranding }}" hint-placeholder-val="false"><div class="cb-detail-brand-panel {{ cbBrandPanelClass }}">
6926                <sc-if value="{{ cbManyMainBrands }}" hint-placeholder-val="false"><div class="cb-detail-brand-heading">CABAÑAS PARTICIPANTES <span>{{ cbMainBrandCount }}</span></div></sc-if>
6927                <sc-if value="{{ cbFewMainBrands }}"><div class="cb-detail-main-logos" aria-label="Cabañas participantes"><sc-for list="{{ cbMainLogos }}" as="brand" hint-placeholder-count="2"><div class="cb-detail-main-brand" title="{{ brand.name }}"><sc-if value="{{ brand.hasLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="{{ brand.name }}" style="--cb-logo-filter:{{ brand.filter }};"></sc-if><sc-if value="{{ brand.noLogo }}" hint-placeholder-val="false"><strong>{{ brand.name }}</strong></sc-if></div></sc-for></div></sc-if>
6928                <sc-if value="{{ cbManyMainBrands }}"><div class="cb-name-roll {{ cbNameRollPausedClass }}">
6929                  <div class="cb-name-roll-window" tabindex="0" role="region" aria-label="Cabañas participantes"><div class="cb-name-roll-track" style="--cb-roll-duration:{{ cbNameRollDuration }};">
6930                    <div class="cb-name-roll-group"><sc-for list="{{ cbMainLogos }}" as="brand"><div class="cb-name-roll-item"><strong>{{ brand.name }}</strong></div></sc-for></div>
6931                    <div class="cb-name-roll-group cb-name-roll-copy" aria-hidden="true"><sc-for list="{{ cbMainLogos }}" as="brand"><div class="cb-name-roll-item"><strong>{{ brand.name }}</strong></div></sc-for></div>
6932                  </div></div>
6933                  <div class="cb-name-roll-footer"><span>Conocé las cabañas del remate</span><button type="button" onClick="{{ cbToggleNameRoll }}" aria-pressed="{{ cbNameRollPaused }}">{{ cbNameRollPauseLabel }}</button></div>
6934                </div></sc-if>
6935                <sc-if value="{{ cbManyGuestBrands }}"><div class="cb-name-roll {{ cbGuestRollPausedClass }}">
6936                  <div class="cb-detail-brand-heading">CABAÑAS INVITADAS <span>{{ cbGuestBrandCount }}</span></div>
6937                  <div class="cb-name-roll-window" tabindex="0" role="region" aria-label="Cabañas invitadas"><div class="cb-name-roll-track" style="--cb-roll-duration:{{ cbGuestRollDuration }};">
6938                    <div class="cb-name-roll-group"><sc-for list="{{ cbGuestLogos }}" as="brand"><div class="cb-name-roll-item"><sc-if value="{{ brand.hasLogo }}"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="" style="width:64px;height:36px;flex:none;object-fit:contain;filter:{{ brand.filter }};"></sc-if><strong>{{ brand.name }}</strong></div></sc-for></div>
6939                    <div class="cb-name-roll-group cb-name-roll-copy" aria-hidden="true"><sc-for list="{{ cbGuestLogos }}" as="brand"><div class="cb-name-roll-item"><sc-if value="{{ brand.hasLogo }}"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="" style="width:64px;height:36px;flex:none;object-fit:contain;filter:{{ brand.filter }};"></sc-if><strong>{{ brand.name }}</strong></div></sc-for></div>
6940                  </div></div>
6941                  <div class="cb-name-roll-footer"><span>Conocé las cabañas invitadas</span><button type="button" onClick="{{ cbToggleGuestRoll }}" aria-pressed="{{ cbGuestRollPaused }}">{{ cbGuestRollPauseLabel }}</button></div>
6942                </div></sc-if>
6943                <sc-if value="{{ cbFewGuestBrands }}" hint-placeholder-val="false"><div class="cb-detail-guests"><span>CABAÑAS INVITADAS</span><sc-for list="{{ cbGuestLogos }}" as="brand" hint-placeholder-count="3"><span class="cb-detail-guest-logo" title="{{ brand.name }}"><sc-if value="{{ brand.hasLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="{{ brand.name }}" style="filter:{{ brand.filter }};"></sc-if><sc-if value="{{ brand.noLogo }}" hint-placeholder-val="false"><small>{{ brand.name }}</small></sc-if></span></sc-for></div></sc-if>
6944              </div></sc-if>
6945            </div>
6946          </div>
6947        </section>
6948        <section style="max-width:1240px; margin:0 auto; padding:22px clamp(14px,4vw,36px) 100px;">
6949          <div style="display:flex; align-items:center; gap:13px; padding:15px 17px; border-radius:15px; color:#fff;
6949 background:{{ cbConsignatariaColor }}; box-shadow:0 10px 28px {{ cbConsignatariaShadow }}; margin-bottom:22px;"><span class="dc-consignataria-logo-box" style="width:42px; height:42px; flex:0 0 42px; font-weight:900;"><sc-if value="{{ cbHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cbConsignatariaWhiteLogo }}" alt="{{ cbConsignataria }}"></sc-if><sc-if value="{{ cbNoConsignatariaWhiteLogo }}" hint-placeholder-val="false">{{ cbConsignatariaInitial }}</sc-if></span><div style="min-width:0;"><small style="display:block; color:rgba(255,255,255,.78); font-weight:800; letter-spacing:.08em;">CONSIGNATARIA</small><strong style="display:block; color:#fff; line-height:1.2;">{{ cbConsignataria }}</strong></div></div>
6950          <div class="dc-catalog-contact-row"><section class="dc-lot-contact-card"><div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar" style="background:{{ cbConsignatariaColor }};"><sc-if value="{{ cbHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cbConsignatariaWhiteLogo }}" alt="{{ cbConsignataria }}"></sc-if><sc-if value="{{ cbNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ cbConsignatariaInitial }}</strong></sc-if></span><span class="dc-lot-contact-copy"><small>CONTACTO DE LOS LOTES</small><strong>{{ cbContactName }}</strong><span>{{ cbContactPhone }}</span></span></div><div class="dc-lot-contact-actions"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ cbContactWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">Consultar por WhatsApp</a><a class="dc-contact-call" data-dc-attr-href="{{ cbContactPhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></div></section></div>
6951          <div class="cb-detail-actions" role="group" aria-label="Acciones del remate">
6952            <sc-if value="{{ cbHasCatalogPdf }}" hint-placeholder-val="false"><a class="remate-list-catalog rd-catalog-cta" data-dc-attr-href="{{ cbCatalogPdfUrl }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/lucide/file-text.svg" alt="" aria-hidden="true"><span>Catálogo</span></a></sc-if>
6953          <sc-if value="{{ cbTermsVisible }}" hint-placeholder-val="false"><div class="cb-terms-entry"><button class="rosgan-page-action" type="button" onClick="{{ cbOpenTerms }}"><img src="assets/icons/lucide/file-text.svg" alt="">Condiciones comerciales</button></div></sc-if>
6954          <sc-if value="{{ cbPrebidSummaryAllowed }}" hint-placeholder-val="false"><nav class="cb-auction-subpages" aria-label="Resumen privado"><button class="rosgan-page-action" onClick="{{ cbOpenPrebidSummary }}"><img src="assets/icons/lucide/file-text.svg" alt="">Resumen de preofertas</button></nav></sc-if>
6955          <sc-if value="{{ cbShowAuctionSubpages }}" hint-placeholder-val="false"><nav class="cb-auction-subpages" aria-label="Secciones del remate">
6956            <sc-if value="{{ cbCanAudit }}"><button class="rosgan-auditor-entry rosgan-page-action" onClick="{{ toggleCbAuditor }}"><img src="assets/icons/lucide/clipboard-list.svg" alt="">Panel auditor</button></sc-if>
6957            <sc-if value="{{ cbCanSeeStatsButton }}" hint-placeholder-val="false"><button class="rosgan-page-action" onClick="{{ openCbStats }}" title="Ver estadísticas del remate"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="">Estadísticas</button></sc-if>
6958            <sc-if value="{{ cbCanManagePrivatePages }}" hint-placeholder-val="false"><button class="rosgan-page-action" onClick="{{ openCbCommercial }}" title="Abrir gestión comercial"><img src="assets/icons/lucide/table-2.svg" alt="">Comercial</button><button class="rosgan-page-action" onClick="{{ openCbPrices }}" title="Ver placa de precios"><img src="assets/icons/lucide/badge-dollar-sign.svg" alt="">Precios</button><button class="rosgan-page-action" onClick="{{ openCbTv }}" title="Abrir salida de TV"><img src="assets/icons/lucide/radio-tower.svg" alt="">TV</button><button class="rosgan-page-action" onClick="{{ openCbTvConfig }}" title="Configurar salida de TV"><img src="assets/icons/lucide/settings.svg" alt="">Config. TV</button></sc-if>
6959          </nav></sc-if>
6960          </div>
6961          <sc-if value="{{ cbCatalogUnlocked }}">
6962          <div style="display:flex; align-items:end; justify-content:space-between; gap:14px; flex-wrap:wrap;"><div><small style="color:{{ cbColor }}; font-weight:900; letter-spacing:.12em;">CATÁLOGO</small><h2 style="margin:3px 0 0; color:#112033;">{{ cbCatalogTitle }}</h2></div><div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;"><sc-if value="{{ cbCanExportCatalog }}" hint-placeholder-val="false"><div class="dc-catalog-export-actions"><button class="dc-catalog-export-button is-excel" onClick="{{ downloadCbCatalogExcel }}" title="Descargar Excel del catalogo"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Descargar Excel</button></div></sc-if><sc-if value="{{ cbCanSeeStatsButton }}" hint-placeholder-val="false"><button onClick="{{ openCbStats }}" title="Ver estadísticas del remate" style="display:inline-flex; align-items:center; gap:8px; border:1px solid #cbd8e7; border-radius:9px; padding:9px 12px; color:#12304d; background:#fff; font:800 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="" style="width:15px; height:15px;">Estadísticas</button></sc-if><sc-if value="{{ cbCanManageSalesOrder }}"><button onClick="{{ openCbSalesOrder }}" title="Ordenar los lotes del remate" style="display:inline-flex; align-items:center; gap:8px; border:1px solid #cbd8e7; border-radius:9px; padding:9px 12px; color:#12304d; background:#fff; font:800 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/list-ordered.svg" alt="" style="width:15px; height:15px;">
6962Orden de ventas</button></sc-if><sc-if value="{{ cbCountsLoading }}"><span class="dc-skeleton" style="width:72px; height:16px; border-radius:99px;"></span></sc-if><sc-if value="{{ cbCountsReady }}"><span style="font-size:13px; color:#65758a;">Todos {{ cbTotal }}</span></sc-if></div></div>
6963          <div style="display:flex; gap:10px; flex-wrap:wrap; margin:14px 0 18px;"><label style="flex:1; min-width:220px; background:#fff; border:1px solid #dce5ef; border-radius:12px; padding:10px 13px;"><input value="" data-dc-attr-value="{{ cbQuery }}" onInput="{{ onCbQuery }}" placeholder="Buscar por RP, nombre o categoría" style="width:100%; border:0; outline:0; font:14px inherit;"></label></div>
6964          <sc-if value="{{ cbLoading }}" hint-placeholder-val="false"><div style="height:220px; border-radius:16px; background:linear-gradient(100deg,#e9eff6 20%,#f7f9fc 40%,#e9eff6 60%); background-size:220% 100%; animation:shimmer 1.5s infinite;"></div></sc-if>
6965          <sc-if value="{{ cbHasLots }}" hint-placeholder-val="true">
6966            <div class="cb-catalog-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(245px,1fr)); gap:16px;">
6967              <sc-for list="{{ cbLots }}" as="lot" hint-placeholder-count="8">
6968                <article class="dc-cabana-catalog-card" onClick="{{ lot.open }}" style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; overflow:hidden; cursor:pointer; opacity:{{ lot.opacity }}; box-shadow:0 8px 24px rgba(0,30,60,.07);">
6969                  <div class="cb-catalog-card-cover" style="height:155px; position:relative; overflow:hidden; background:linear-gradient(145deg,#0a5ca8,#002e5f);">
6970                    <sc-if value="{{ lot.hasPreviousMediaLayer }}" hint-placeholder-val="false"><div class="dc-cabana-card-media" style="position:absolute; inset:0; opacity:{{ lot.previousMediaOpacity }};">
6971                      <sc-if value="{{ lot.previousHasMedia }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lot.previousMedia }}" alt="" style="width:100%; height:100%; object-fit:cover;"></sc-if>
6972                      <sc-if value="{{ lot.previousHasBackgroundLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lot.previousBackgroundLogo }}" alt="" style="position:absolute; inset:18px; width:calc(100% - 36px); height:calc(100% - 36px); object-fit:contain; opacity:.24; filter:{{ lot.previousBackgroundLogoFilter }};"></sc-if>
6973                    </div></sc-if>
6974                    <div class="dc-cabana-card-media" style="position:absolute; inset:0; opacity:{{ lot.mediaOpacity }};">
6975                      <sc-if value="{{ lot.hasMedia }}" hint-placeholder-val="false"><img class="dc-media-image" data-dc-attr-src="{{ lot.media }}" onLoad="{{ lot.mediaReady }}" onError="{{ lot.mediaError }}" alt="" style="width:100%; height:100%; object-fit:cover;"></sc-if>
6976                      <sc-if value="{{ lot.hasVideoPreview }}" hint-placeholder-val="false"><video data-dc-attr-src="{{ lot.videoPreview }}" onCanPlay="{{ lot.mediaReady }}" onError="{{ lot.mediaError }}" muted autoplay loop playsinline preload="metadata" style="width:100%; height:100%; object-fit:cover;"></video></sc-if>
6977                      <sc-if value="{{ lot.hasBackgroundLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lot.backgroundLogo }}" alt="" style="position:absolute; inset:18px; width:calc(100% - 36px); height:calc(100% - 36px); object-fit:contain; opacity:.24; filter:{{ lot.backgroundLogoFilter }};"></sc-if>
6978                    </div>
6979                    <sc-if value="{{ lot.videoUploadBusy }}" hint-placeholder-val="false"><div class="dc-cabana-card-upload-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" data-dc-attr-aria-valuenow="{{ lot.videoUploadProgress }}" onClick="{{ lot.stopUploadClick }}"><span><b>{{ lot.videoUploadStage }}</b><strong>{{ lot.videoUploadProgress }}%</strong></span><div class="dc-cabana-upload-track"><span style="width:{{ lot.videoUploadProgress }}%;"></span></div><small>No cierres esta pantalla.</small></div></sc-if>
6980                    <sc-if value="{{ lot.canUploadVideo }}" hint-placeholder-val="false"><label class="dc-cabana-card-upload" onClick="{{ lot.stopUploadClick }}"><img src="assets/icons/lucide/upload.svg" alt="">Subir video<input type="file" accept="video/*" multiple onChange="{{ lot.uploadVideo }}" style="display:none;"></label></sc-if>
6981                    <div style="position:absolute; inset:0; background:linear-gradient(180deg,transparent,rgba(0,29,61,.78));"></div>
6982                    <sc-if value="{{ lot.hasRp }}"><span class="dc-cabana-card-rp" data-dc-attr-title="{{ lot.rpTitle }}" style="position:absolute; top:10px; left:10px;
6982 background:{{ cbColor }}; color:#fff; border-radius:100px; padding:5px 10px; font-size:11px; font-weight:900;">{{ lot.rp }}</span></sc-if>
6983                    <sc-if value="{{ lot.hasLotNumber }}" hint-placeholder-val="true"><span class="cb-catalog-card-number" style="position:absolute; top:10px; right:10px; background:rgba(0,24,49,.82); color:#fff; border:1px solid rgba(255,255,255,.48); border-radius:100px; padding:5px 10px; font-size:11px; font-weight:900; letter-spacing:.04em; backdrop-filter:blur(6px);">{{ lot.lotBadge }}</span></sc-if>
6984                    <div style="position:absolute; left:12px; right:12px; bottom:10px; color:#fff;"><sc-if value="{{ lot.showName }}" hint-placeholder-val="false"><strong class="dc-cabana-card-title" style="display:block; font-size:16px;">{{ lot.name }}</strong></sc-if><span style="font-size:11px; opacity:.85;">{{ lot.category }} - {{ lot.breed }}</span></div>
6985                  </div>
6986<sc-if value="{{ lot.canUploadVideo }}" hint-placeholder-val="false"><div style="padding:8px 13px;" onClick="{{ lot.stopUploadClick }}"><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a></div></sc-if>
6987                  <sc-if value="{{ lot.hasCardMetaRow }}"><div style="display:flex; align-items:center; justify-content:space-between; gap:10px; padding:9px 13px 0;"><span style="display:flex; align-items:center; gap:8px; min-width:0;"><sc-if value="{{ lot.hasVariants }}"><small style="color:#65758a; font-size:10px; font-weight:800; letter-spacing:.06em;">{{ lot.variantPosition }}</small></sc-if><sc-if value="{{ lot.hasRankBadge }}" hint-placeholder-val="false"><span title="{{ lot.rankTitle }}" style="display:inline-flex; align-items:center; gap:5px; min-width:0; padding:4px 8px; border-radius:100px; background:{{ lot.rankBg }}; color:{{ lot.rankColor }}; border:1px solid {{ lot.rankBorder }}; font-size:10px; font-weight:950; letter-spacing:.03em; text-transform:uppercase;"><img data-dc-attr-src="{{ lot.rankIconUrl }}" alt="" style="width:12px; height:12px; flex:0 0 auto;">{{ lot.rankLabel }}</span></sc-if></span><sc-if value="{{ lot.hasVariants }}"><div style="display:flex; align-items:center; gap:5px;"><sc-for list="{{ lot.dots }}" as="dot"><button onClick="{{ dot.select }}" title="{{ dot.label }}" style="width:{{ dot.width }}; height:7px; padding:0; border:0; border-radius:999px; background:{{ dot.bg }}; cursor:pointer; transition:width .24s ease, background .24s ease;"></button></sc-for></div></sc-if></div></sc-if>
6988                  <div class="cb-catalog-card-body" style="padding:13px;"><div class="cb-catalog-card-facts" style="display:grid; grid-template-columns:1fr 1fr; gap:9px;"><sc-for list="{{ lot.facts }}" as="fact" hint-placeholder-count="4"><div class="dc-cabana-card-fact"><small style="display:block; color:#8593a6;">{{ fact.label }}</small><strong data-dc-attr-title="{{ fact.title }}" style="font-size:13px;">{{ fact.value }}</strong></div></sc-for></div><sc-if value="{{ lot.showBase }}" hint-placeholder-val="false"><div class="cb-catalog-card-base" style="display:flex; justify-content:space-between; margin-top:12px; padding-top:10px; border-top:1px solid #edf2f7;"><small style="color:{{ lot.priceColor }}; font-weight:{{ lot.priceWeight }};">{{ lot.priceLabel }}</small><strong style="color:{{ lot.priceColor }};">{{ lot.base }}</strong></div></sc-if></div>
6989                </article>
6990              </sc-for>
6991            </div>
6992          </sc-if>
6993          <sc-if value="{{ cbLotsEmpty }}" hint-placeholder-val="false"><div style="padding:42px 22px; border:1px dashed #cbd8e7; border-radius:17px; background:#fff; text-align:center;"><strong style="display:block;">No hay lotes publicados para este remate</strong><span style="font-size:13px; color:#65758a;">El catálogo aparecerá cuando existan lotes reales asociados.</span></div></sc-if>
6994          </sc-if>
6995        </section>
6996        <div class="dc-catalog-floating-contact" aria-hidden="true"><span class="dc-catalog-floating-avatar" style="background:{{ cbC
6996onsignatariaColor }};"><sc-if value="{{ cbHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cbConsignatariaWhiteLogo }}" alt="{{ cbConsignataria }}"></sc-if><sc-if value="{{ cbNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ cbConsignatariaInitial }}</strong></sc-if></span><span class="dc-catalog-floating-copy"><small>CONTACTO DE LOS LOTES</small><strong>{{ cbContactName }}</strong></span><a class="dc-contact-whatsapp" data-dc-attr-href="{{ cbContactWhatsappHref }}" target="_blank" rel="noopener noreferrer" aria-label="Consultar por WhatsApp"><img src="assets/icons/brands/whatsapp.svg" alt=""></a><a class="dc-contact-call" data-dc-attr-href="{{ cbContactPhoneHref }}" aria-label="Llamar"><img src="assets/icons/lucide/phone.svg" alt=""></a></div>
6997      </main>
6998    </sc-if>
6999    <sc-if value="{{ isCabanaPrebidPanel }}" hint-placeholder-val="false">
7000      <main class="cb-prebid-page">
7001        <section class="cb-prebid-page-shell">
7002          <button class="cb-prebid-page-back" onClick="{{ closeCbPrebidPanel }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al remate</button>
7003          <header class="cb-prebid-page-title">
7004            <div><small>GESTIÓN PRIVADA · CABAÑAS</small><h1>Preofertas de {{ cbTitle }}</h1><p>Seguimiento en tiempo real con identidad, lote y animales involucrados.</p></div>
7005            <span class="cb-prebid-page-live"><i class="cb-prebid-admin-live"></i>{{ cbPrebidPanelUpdated }}</span>
7006          </header>
7007          <section class="cb-prebid-admin-panel is-page">
7008            <header class="cb-prebid-admin-head">
7009              <div><small>PANEL DE CONTROL</small><h2>Actividad de preofertas</h2><p>Información privada visible sólo para perfiles autorizados.</p></div>
7010              <div class="cb-prebid-admin-head-actions"><button onClick="{{ refreshCbPrebidPanel }}" title="Actualizar ahora"><img src="assets/icons/lucide/refresh-cw.svg" alt=""></button></div>
7011            </header>
7012            <sc-if value="{{ cbPrebidPanelHasLots }}" hint-placeholder-val="true"><section class="cb-prebid-lot-picker"><div class="cb-prebid-lot-picker-head"><div><small>LOTES DEL REMATE</small><strong>Elegí un lote para ver sus preofertas</strong></div><span>La información se muestra por separado.</span></div><div class="cb-prebid-lot-picker-grid"><sc-for list="{{ cbPrebidPanelLots }}" as="lot" hint-placeholder-count="3"><button class="{{ lot.className }}" onClick="{{ lot.go }}"><span class="cb-prebid-lot-option-main"><strong>{{ lot.label }}</strong><small>{{ lot.meta }}</small></span><span class="cb-prebid-lot-option-side"><b>{{ lot.currentAmount }}</b><span>{{ lot.offerLabel }}</span></span></button></sc-for></div></section></sc-if>
7013            <sc-if value="{{ cbPrebidPanelHasSelectedLot }}" hint-placeholder-val="true"><div class="cb-prebid-selected-context">Mostrando únicamente <strong>{{ cbPrebidPanelSelectedLotLabel }}</strong></div></sc-if>
7014            <div class="cb-prebid-admin-metrics"><sc-for list="{{ cbPrebidPanelMetrics }}" as="metric" hint-placeholder-count="4"><article><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong></article></sc-for></div>
7015            <div class="cb-prebid-admin-toolbar"><label><img src="assets/icons/lucide/search.svg" alt=""><input value="" data-dc-attr-value="{{ cbPrebidPanelQuery }}" onInput="{{ onCbPrebidPanelQuery }}" placeholder="Buscar email, nombre, CUIT, lote, RP, pintura..."></label><div><sc-for list="{{ cbPrebidPanelStatusTabs }}" as="tab" hint-placeholder-count="4"><button class="{{ tab.className }}" onClick="{{ tab.go }}">{{ tab.label }}</button></sc-for></div></div>
7016            <sc-if value="{{ cbPrebidPanelLoading }}" hint-placeholder-val="false"><div class="cb-prebid-admin-message"><span class="dc-spinner"></span> Cargando preofertas...</div></sc-if>
7017            <sc-if value="{{ cbPrebidPanelHasError }}" hint-placeholder-val="false"><div class="cb-prebid-admin-message is-error">{{ cbPrebidPanelError }}</div></sc-if>
7018            <sc-if value="{{ cbPrebidPanelHasRows }}" hint-placeholder-val="true"><div class="cb-prebid-admin-list"><sc-for list="{{ cbPrebidPanelRows }}" as="offer" hint-placeholder-count="5"><article class="cb-prebid-admin-row"><div class="cb-prebid-admin-offer"><span class="{{ offer.statusClass }}">{{ offer.status }}</span><strong>{{ offer.amount }}</strong><small>{{ offer.date }}</small><sc-if value="{{ offer.canDelete }}"><button disabled="{{ offer.deleteBusy }}" onClick="{{ offer.deleteOffer }}" style="padding:8px 12px; border:1px solid #efb7bf; border-radius:9px; color:#b51f35; background:#fff; cursor:pointer;">Eliminar preoferta</button></sc-if><sc-if value="{{ offer.canApprove }}"><div class="cb-contract-actions"><button disabled="{{ offer.busy }}" onClick="{{ offer.approve }}">Aprobar</button><button disabled="{{ offer.busy }}" onClick="{{ offer.reject }}">Rechazar</button></div></sc-if></div><div class="cb-prebid-admin-buyer"><small>OFERTANTE</small><strong>{{ offer.buyerName }}</strong><a data-dc-attr-href="mailto:{{ offer.buyerEmail }}">{{ offer.buyerEmail }}</a><span>{{ offer.buyerCuit }}</span><sc-if value="{{ offer.hasCommission }}" hint-placeholder-val="false"><em>{{ offer.commission }}</em></sc-if></div><div class="cb-prebid-admin-lot"><small>LOTE / CABAÑA</small><strong>{{ offer.lot }}</strong><span>{{ offer.lotMeta }}</span><sc-if value="{{ offer.hasAnimals }}" hint-placeholder-val="true"><div class="cb-prebid-admin-animals"><sc-for list="{{ offer.animals }}" as="animal" hint-placeholder-count="2"><span><b>{{ animal.cert }}</b>{{ animal.detail }}</span></sc-for></div></sc-if></div></article></sc-for></div></sc-if>
7019            <sc-if value="{{ cbPrebidPanelEmpty }}" hint-placeholder-val="false"><div class="cb-prebid-admin-message">Este lote todavía no tiene preofertas para los filtros seleccionados.</div></sc-if>
7020          </section>
7021        </section>
7022      </main>
7023    </sc-if>
7024    <sc-if value="{{ isRosganStats }}"><main class="rosgan-ops-page"><div class="rosgan-ops-shell"><button class="rosgan-ops-back" onClick="{{ rosOpsBack }}">← Volver al catálogo</button><rosgan-audience-dashboard remate-id="{{ rgAudienceId }}"></rosgan-audience-dashboard></div></main></sc-if>
7025    <sc-if value="{{ isRosganCommercial }}" hint-placeholder-val="false">
7026      <main class="commercial-page rg-commercial-page"><div class="commercial-shell">
7027        <button class="commercial-back" onClick="{{ rosOpsBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button>
7028        <header class="commercial-hero"><div><span class="commercial-kicker">MESA COMERCIAL · ROSGAN</span><h1>Carga comercial</h1><p>{{ rosOpsTitle }} · valores por lote y certificado</p></div><div class="commercial-hero-actions"><sc-if value="{{ rosCanCloseRemate }}"><button type="button" class="commercial-prices-link" onClick="{{ rosToggleRemateClosed }}" disabled="{{ rosRemateClosingBusy }}">{{ rosRemateClosedLabel }}</button></sc-if>
7029          <button class="commercial-prices-link" onClick="{{ rosOpsOpenPrices }}"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="">Placa de precios</button>
7030          <button class="commercial-prices-link" onClick="{{ rosOpsOpenTvConfig }}"><img src="assets/icons/lucide/settings.svg" alt="">Configurar TV</button>
7031          <button class="commercial-prices-link" onClick="{{ rosOpsOpenTv }}"><img src="assets/icons/lucide/video.svg" alt="">Abrir salida TV</button>
7032          <button class="commercial-prices-link" onClick="{{ rosExportAirtime }}" title="Descargar informe ejecutivo Excel de tiempos"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Tiempos Excel</button>
7033          <button class="commercial-prices-link" onClick="{{ rosBillingExcel }}"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Facturación Excel</button><button class="commercial-prices-link" onClick="{{ rosBillingPdf }}"><img src="assets/icons/lucide/file-text.svg" alt="">Facturación PDF</button><span class="commercial-autosave-note"><img src="assets/icons/lucide/save.svg" alt="">Autoguardado</span>
7034        </div></header>
7035        <section class="commercial-summary" aria-label="Resumen comercial"><sc-for list="{{ rosCommercialMetrics }}" as="metric"><article class="commercial-metric"><span class="commercial-metric-icon" style="background:{{ metric.bg }}"><img data-dc-attr-src="{{ metric.icon }}" alt=""></span><span><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong></span></article></sc-for></section>
7036        <section class="rg-commercial-progress" aria-label="Distribución de estados del remate"><header><strong>Estado del remate</strong><span>{{ rosProgressTotal }}</span></header><div class="rg-progress-track" role="img" aria-label="{{ rosProgressDescription }}"><sc-for list="{{ rosProgressSegments }}" as="segment"><span class="rg-progress-segment" style="width:{{ segment.width }}%;background:{{ segment.color }}" title="{{ segment.label }}: {{ segment.percent }} · {{ segment.count }} lotes"></span></sc-for></div><div class="rg-progress-legend"><sc-for list="{{ rosProgressSegments }}" as="segment"><span><i style="background:{{ segment.color }}"></i>{{ segment.label }} <strong>{{ segment.percent }}</strong><small>({{ segment.count }})</small></span></sc-for></div></section>
7037        <section class="rg-airtime-panel" aria-label="Tiempos al aire"><header><strong>Cronómetro del remate</strong><span>{{ rosAirtimeStatus }}</span><button onClick="{{ rosAirtimeResetAll }}" class="rg-time-button">Reiniciar todos</button></header><div class="rg-airtime-metrics"><sc-for list="{{ rosAirtimeMetrics }}" as="metric"><div><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong></div></sc-for></div><details><summary>Tiempos por consignataria</summary><div class="rg-airtime-groups"><sc-for list="{{ rosAirtimeGroups }}" as="group"><article><div class="rg-airtime-group-heading"><span class="rg-airtime-group-logo" style="background:{{ group.color }}"><sc-if value="{{ group.hasLogo }}"><img src="{{ group.logo }}" alt="" loading="lazy" decoding="async"></sc-if><sc-if value="{{ group.noLogo }}">{{ group.initial }}</sc-if></span><strong>{{ group.name }}</strong></div><span>Total <b>{{ group.totalLabel }}</b></span><span>Promedio <b>{{ group.averageLabel }}</b></span><small>{{ group.count }} lotes cronometrados</small></article></sc-for></div></details><p>Promedios sobre lotes con tiempo registrado. Fin estimado: promedio × lotes pendientes, sin paus
7037as. En pantalla múltiple, el tiempo se reparte por igual entre los lotes visibles.</p></section>
7038        <div class="rg-commercial-tools"><nav class="commercial-filters" aria-label="Filtrar por estado"><sc-for list="{{ rosCommercialFilters }}" as="filter"><button class="{{ filter.className }}" aria-pressed="{{ filter.active }}" onClick="{{ filter.select }}">{{ filter.label }}<b>{{ filter.count }}</b></button></sc-for></nav><label class="rg-commercial-search"><img src="assets/icons/lucide/search.svg" alt=""><input type="search" aria-label="Buscar lote comercial" data-dc-attr-value="{{ rosCommercialQuery }}" onInput="{{ rosCommercialSearch }}" placeholder="Lote, certificado o consignataria"></label></div>
7039        <div class="rg-commercial-caption"><strong>Carga de precios</strong><span>{{ rosCommercialResultCount }} de {{ rosOpsLotCount }} lotes</span></div>
7040        <section class="commercial-board" aria-label="Carga comercial por lote">
7041          <div class="rg-commercial-head"><span>Orden</span><span>Lote / consignataria</span><span>Datos del lote</span><span>Estado</span><span>Precio</span></div>
7042          <sc-if value="{{ rosOpsLoading }}"><div class="commercial-empty"><span class="dc-spinner"></span> Cargando lotes...</div></sc-if>
7043          <sc-for list="{{ rosCommercialRows }}" as="row"><article key="{{ row.cert }}" class="commercial-row rg-commercial-row {{ row.commercialClass }}">
7044            <div class="rg-commercial-order"><span class="commercial-order-badge">{{ row.order }}</span></div>
7045            <div class="rg-commercial-identity"><small>Cert. {{ row.cert }}</small><strong>{{ row.category }}</strong><span class="rg-commercial-breed">{{ row.breed }}</span><span class="rg-commercial-cons"><span class="commercial-cons-avatar" style="background:{{ row.consColor }}"><sc-if value="{{ row.hasConsLogo }}"><img data-dc-attr-src="{{ row.consLogo }}" alt=""></sc-if><sc-if value="{{ row.noConsLogo }}"><strong>{{ row.consInitial }}</strong></sc-if></span><span>{{ row.consignataria }}</span></span></div>
7046            <div class="rg-commercial-data"><strong>{{ row.dataSummary }}</strong><span>{{ row.locality }}</span><small>Plazo: {{ row.plazo }}</small><span class="rg-lot-interest" title="Personas que solicitaron el aviso de salida, incluidas las ya notificadas."><img src="/assets/icons/lucide/bell.svg" alt="" aria-hidden="true">{{ row.interestLabel }}</span><span class="rg-airtime-lot">{{ row.airtimeState }} · <b>{{ row.airtimeLabel }}</b><button class="rg-time-button" aria-label="Editar tiempo del lote {{ row.order }}" title="Editar tiempo" onClick="{{ row.editTime }}">✎</button><button class="rg-time-button" aria-label="Reiniciar tiempo del lote {{ row.order }}" title="Reiniciar tiempo" onClick="{{ row.resetTime }}">↺</button></span><sc-if value="{{ row.editingTime }}"><div class="rg-time-editor"><input aria-label="Tiempo del lote {{ row.order }}" placeholder="HH:MM:SS" data-dc-attr-value="{{ row.timeDraft }}" onInput="{{ row.changeTime }}"><button class="rg-time-button" onClick="{{ row.saveTime }}">Guardar</button><button class="rg-time-button" onClick="{{ row.cancelTime }}">Cancelar</button></div></sc-if></div>
7047            <div class="rg-commercial-status"><span class="{{ row.statusClass }}">{{ row.statusLabel }}</span><div class="commercial-status"><button class="{{ row.pendingClass }}" aria-pressed="{{ row.isPending }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setPending }}">Pendiente</button><button class="{{ row.soldClass }}" aria-pressed="{{ row.isSold }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setSold }}">✓ Vendido</button><button class="{{ row.unsoldClass }}" aria-pressed="{{ row.isUnsold }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setUnsold }}">No vendido</button><button class="{{ row.liftedClass }}" aria-pressed="{{ row.isLifted }}" onClick="{{ row.setLifted }}">Levantado</button></div></div>
7048            <div class="commercial-price"><div class="commercial-mode"><button class="{{ row.kiloClass }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setKilo }}">$ / kg</button><button class="{{ row.headClass }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setHead }}">$ / cabeza</button></div><input type="text" disabled="{{ row.hasDivisions }}" aria-label="{{ row.priceLabel }}" data-dc-attr-value="{{ row.priceValue }}" onInput="{{ row.setPrice }}" onBlur="{{ row.commitPrice }}" onKeyDown="{{ row.commitPriceEnter }}" inputmode="decimal" placeholder="Ingresar precio"></div><label style="grid-column:1 / -1;display:flex;align-items:center;gap:10px;font-size:12px;color:#526b82;">Comprador (opcional)<input type="text" disabled="{{ row.hasDivisions }}" maxlength="200" aria-label="Comprador del lote {{ row.order }}" placeholder="Nombre o razón social" data-dc-attr-value="{{ row.buyerValue }}" onInput="{{ row.setBuyer }}" onBlur="{{ row.commitPrice }}" style="flex:1;min-width:0;padding:9px 11px;border:1px solid #cad8e6;border-radius:8px;font:inherit;"></label><button type="button" onClick="{{ row.openDivisions }}" style="grid-column:1 / -1;justify-self:end;border:1px solid #b9cddd;border-radius:8px;padding:9px 14px;background:#edf5fb;color:#073763;font:700 12px Arial;cursor:pointer">{{ row.divisionLabel }}</button>
7049          </article></sc-for>
7050          <sc-if value="{{ rosCommercialEmpty }}"><div class="commercial-empty">No hay lotes que coincidan con la búsqueda o el estado seleccionado.</div></sc-if>
7051        </section>
7052      </div></main>
7053    </sc-if>
7054
7055    <sc-if value="{{ isRosganPriceBoard }}" hint-placeholder-val="false">
7056      <main class="price-board-page"><div class="price-board-shell">
7057        <button class="commercial-back" onClick="{{ rosOpsBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button>
7058<div class="price-board-toolbar"><sc-if value="{{ rosOpsCanAdmin }}" hint-placeholder-val="false"><button class="commercial-prices-link" onClick="{{ rosOpsOpenCommercial }}"><img src="/assets/icons/lucide/badge-dollar-sign.svg" alt="" aria-hidden="true">Carga comercial</button><button class="commercial-prices-link" onClick="{{ rosOpsOpenTvConfig }}"><img src="/assets/icons/lucide/settings.svg" alt="" aria-hidden="true">Configurar TV</button><button class="commercial-prices-link" onClick="{{ rosOpsOpenTv }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true">Abrir salida TV</button></sc-if><button class="commercial-prices-link price-board-story-export" onClick="{{ rosPriceExport }}" disabled="{{ rosPriceExportBusy }}" title="Descargar precios vendidos en imágenes PNG de 1080 × 1920"><img src="assets/icons/brands/instagram.svg" alt="">{{ rosPriceExportLabel }}</button><span class="price-board-updated">{{ rosPriceSource }}</span></div>
7059        <div class="price-board-view-switch" aria-label="Tipo de placa"><button onClick="{{ rosPriceCategoryView }}" aria-pressed="{{ rosPriceCategorySelected }}">Resultados por categoría</button><button onClick="{{ rosPriceWeightView }}" aria-pressed="{{ rosPriceWeightSelected }}">Por rango de kilaje</button></div>
7060        <p role="status">{{ rosPriceExportMessage }}</p><rosgan-price-board data-board="{{ rosPriceBoardData }}"></rosgan-price-board>
7061
7062      </div></main>
7063    </sc-if>
7064
7065    <sc-if value="{{ isRosganTvConfig }}" hint-placeholder-val="false">
7066      <main class="rosgan-ops-page">
7067        <div class="rosgan-ops-shell">
7068          <button class="rosgan-ops-back" onClick="{{ rosOpsBack }}"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Volver al catálogo</button>
7069          <header class="rosgan-ops-hero"><div><small>CONTROL TV · ROSGAN</small><h1>Gráfica del remate</h1><p>{{ rosOpsTitle }} · los cambios se reflejan en la salida TV</p></div><div class="rosgan-ops-actions"><button onClick="{{ rosOpsOpenCommercial }}"><img src="/assets/icons/lucide/badge-dollar-sign.svg" alt="" aria-hidden="true">Comercial</button><button onClick="{{ rosOpsOpenPrices }}"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="" aria-hidden="true">Precios</button><button class="is-primary" onClick="{{ rosTvOpenOutput }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true">Abrir salida TV</button></div></header>
7070          <div class="rosgan-tv-config-grid"><section class="rosgan-tv-config-card"><h2>Lote al aire</h2><div class="rosgan-tv-selector"><button onClick="{{ rosTvPrev }}">Anterior</button><select value="" data-dc-attr-value="{{ rosTvSelectedCert }}" onChange="{{ rosTvSelect }}"><sc-for list="{{ rosTvOptions }}" as="option" hint-placeholder-count="8"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select><button onClick="{{ rosTvNext }}">Siguiente</button></div><span class="rosgan-tv-url">{{ rosTvOutputUrl }}</span>
7070<p class="rosgan-tv-preview-note">El video del lote seleccionado se carga automáticamente, silenciado y en loop. La salida no requiere iniciar la reproducción manualmente.</p></section><section class="rosgan-tv-config-card"><h2>Elementos de pantalla</h2><div class="rosgan-tv-toggle-list"><button class="rosgan-tv-toggle {{ rosTvVideoClass }}" onClick="{{ rosTvToggleVideo }}" aria-pressed="{{ rosTvVideoVisible }}"><span>Videos y datos del lote</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvVisibleClass }}" onClick="{{ rosTvToggleVisible }}"><span>Gráfica visible</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvLogosClass }}" onClick="{{ rosTvToggleLogos }}"><span>Marcas superiores</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvLowerClass }}" onClick="{{ rosTvToggleLower }}"><span>Zócalo del lote</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvContactsClass }}" onClick="{{ rosTvToggleContacts }}"><span>Ticker de contactos</span><i></i></button></div></section></div>
7071          <section class="rosgan-tv-config-card" style="margin:16px 0"><h2>Conexión con vMix</h2><div style="display:flex;gap:12px;align-items:end;flex-wrap:wrap"><label class="ros-tv-edit-field" style="flex:1;min-width:220px">IP de vMix · puerto opcional<input aria-label="IP de vMix" data-dc-attr-value="{{ rosTvVmixHost }}" onInput="{{ rosTvVmixHostChange }}" placeholder="192.168.122.28:8088"></label><label class="ros-tv-edit-field" style="width:140px">Entrada Browser<input type="number" min="1" max="1000" aria-label="Entrada de vMix" data-dc-attr-value="{{ rosTvVmixInput }}" onInput="{{ rosTvVmixInputChange }}"></label><button class="ros-tv-save" onClick="{{ rosTvVmixSave }}" aria-label="Guardar conexión de vMix" title="Guardar conexión de vMix"><img src="assets/icons/lucide/save.svg" alt="" width="18" height="18"></button></div><p class="rosgan-tv-preview-note">Cronómetro y avisos usan el monitor local, que debe permanecer encendido en la red de vMix. Los cambios de IP y entrada se sincronizan desde rosgan.app. Puerto predeterminado: 8088. La entrada debe tener cargada la salida TV vinculada a este remate.</p><button class="rosgan-page-action" onClick="{{ rosTvVmixCopyUrl }}"><img src="/assets/icons/lucide/copy.svg" width="16" height="16" alt="">Copiar URL para vMix</button></section>
7072          <section class="rosgan-tv-config-card" style="margin-top:18px;"><h2>Zócalos de nombres</h2><p style="font-size:12px;color:#68839a;">Guardá nombres y cargos para presentarlos al aire. El zócalo de nombre reemplaza temporalmente los datos de los lotes.</p><div class="rtv-name-editor"><input aria-label="Nombre del zócalo" maxlength="100" placeholder="Nombre y apellido" data-dc-attr-value="{{ rosNameDraft }}" onInput="{{ rosNameChange }}"><input aria-label="Cargo del zócalo" maxlength="120" placeholder="Cargo (opcional)" data-dc-attr-value="{{ rosRoleDraft }}" onInput="{{ rosRoleChange }}"><button onClick="{{ rosNameSave }}">Guardar nombre</button></div><sc-for list="{{ rosNames }}" as="person"><div class="rtv-name-item"><div><strong>{{ person.name }}</strong><small>{{ person.role }}</small></div><button onClick="{{ person.toggle }}" aria-pressed="{{ person.active }}">{{ person.action }}</button><button onClick="{{ person.edit }}">Editar</button><button onClick="{{ person.remove }}" aria-label="Eliminar {{ person.name }}">✕</button></div></sc-for></section>
7073          <section class="rosgan-tv-config-card" style="margin-top:18px;"><h2>Placas del remate</h2><p>La placa ocupa toda la salida. Al quitarla se recupera la gráfica anterior.</p><button class="rosgan-tv-toggle {{ rosWaitClass }}" onClick="{{ rosWaitToggle }}" aria-pressed="{{ rosWaitActive }}">Placa de espera<i></i></button><button class="rosgan-tv-toggle {{ rosEndClass }}" onClick="{{ rosEndToggle }}" aria-pressed="{{ rosEndActive }}">Fin del remate<i></i></button></section>
7074          <div class="rosgan-tv-workspace">
7075            <section class="rosgan-tv-config-card ros-tv-catalog"><div class="ros-tv-catalog-heading"><h2>Todos los lotes · {{ rosOpsLotCount }}</h2><nav class="ros-tv-catalog-arrows" aria-label="Cambiar lote"><button onClick="{{ rosTvPrev }}" aria-label="Lote anterior" title="Lote anterior"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i></button><button onClick="{{ rosTvNext }}" aria-label="Lote siguiente" title="Lote siguiente"><i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></button></nav></div>
7076              <input class="ros-tv-search" aria-label="Buscar lote" placeholder="Lote, certificado, categoría o consignataria" data-dc-attr-value="{{ rosTvQuery }}" onInput="{{ rosTvSearch }}">
7077              <p class="rosgan-tv-preview-note">El seleccionado ocupa la pantalla 1. Usá 2, 3 o 4 para sumar otro lote; tocá el número activo para quitarlo.</p>
7078              <div class="ros-tv-multi-controls"><strong>{{ rosTvLayoutLabel }}</strong><sc-if value="{{ rosTvMulti }}
7078"><button onClick="{{ rosTvClearSlots }}">Volver a 1</button></sc-if></div>
7079              <div class="ros-tv-lot-list"><sc-for list="{{ rosTvLotList }}" as="lot"><div class="ros-tv-lot-row {{ lot.selectedClass }}"><button class="ros-tv-lot-main" disabled="{{ lot.disabled }}" onClick="{{ lot.select }}">
7080                <span class="ros-tv-order">{{ lot.order }}</span><span class="ros-tv-lot-copy"><small>Cert. {{ lot.cert }} <sc-if value="{{ lot.sold }}"><span class="ros-tv-sold-check" role="img" aria-label="Vendido" title="Vendido"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m7.5 12 3 3 6-6"/></svg></span></sc-if><sc-if value="{{ lot.disabled }}"><span class="ros-tv-lifted-label">Levantado</span></sc-if></small><strong>{{ lot.category }}</strong><span>{{ lot.consignataria }}</span></span>
7081                <span class="ros-tv-cons-mark" style="background:{{ lot.color }}"><img data-dc-attr-src="{{ lot.logo }}" alt="" loading="lazy"></span>
7082              </button><span class="ros-tv-slot-buttons"><sc-for list="{{ lot.slots }}" as="slot"><button class="{{ slot.className }}" aria-label="{{ slot.label }}" title="{{ slot.label }}" aria-pressed="{{ slot.active }}" disabled="{{ slot.disabled }}" onClick="{{ slot.select }}">{{ slot.number }}</button></sc-for></span></div></sc-for></div>
7083            </section>
7084            <section class="rosgan-tv-config-card"><div class="ros-tv-editor-heading"><h2>Editar lote · Cert. {{ rosTvSelectedCert }}</h2><button type="button" class="ros-tv-save" aria-label="Guardar cambios del lote" title="Guardar cambios y actualizar TV" aria-disabled="{{ rosTvSaveDisabled }}" onClick="{{ rosTvSave }}"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12l4 4v12a2 2 0 0 1-2 2Z"/><path d="M7 3v6h10V3M7 21v-8h10v8"/></svg></button></div><p class="rosgan-tv-preview-note">Editá los datos y presioná guardar para actualizar el lote original y la salida de TV. El comitente no se puede modificar desde esta pantalla.</p><p role="status" class="ros-tv-save-status">{{ rosTvEditStatus }}</p>
7085              <div class="ros-tv-editor"><sc-for list="{{ rosTvEditorFields }}" as="field"><label key="{{ field.key }}" class="ros-tv-edit-field"><span>{{ field.label }}</span>
7086                <sc-if value="{{ field.isInput }}"><input data-dc-attr-type="{{ field.type }}" min="0" data-dc-attr-value="{{ field.value }}" onInput="{{ field.change }}"></sc-if>
7087                <sc-if value="{{ field.isTextarea }}"><textarea data-dc-attr-value="{{ field.value }}" onInput="{{ field.change }}" rows="3"></textarea></sc-if>
7088                <sc-if value="{{ field.isBoolean }}"><select data-dc-attr-value="{{ field.value }}" onChange="{{ field.change }}"><sc-if value="{{ field.nullableBoolean }}"><option value="">Sin dato</option></sc-if><option value="true">Sí</option><option value="false">No</option></select></sc-if>
7089                <sc-if value="{{ field.isSelect }}"><select data-dc-attr-value="{{ field.value }}" onChange="{{ field.change }}"><sc-for list="{{ field.options }}" as="opt"><option data-dc-attr-value="{{ opt.value }}" data-dc-attr-label="{{ opt.label }}">{{ opt.label }}</option></sc-for></select></sc-if>
7090                <sc-if value="{{ field.isBreeds }}"><span class="ros-tv-breeds"><sc-for list="{{ field.breedOptions }}" as="breed"><button class="{{ breed.selectedClass }}" onClick="{{ breed.toggle }}">{{ breed.label }}</button></sc-for></span></sc-if>
7091              </label></sc-for></div>
7092            </section>
7093          </div>
7094          <section class="rosgan-tv-config-card ros-tv-contacts-section" aria-label="Contactos por consignataria">
7095            <h2>Contactos del ticker por consignataria</h2>
7096            <p class="rosgan-tv-preview-note">Se guardan automáticamente y aparecen en el zócalo de los lotes de cada consignataria.</p>
7097            <div class="ros-tv-contact-grid"><sc-for list="{{ rosTvContactRows }}" as="contact"><label key="{{ contact.key }}" class="ros-tv-contact-card">
7098              <span class="ros-tv-contact-identity"><span class="ros-tv-contact-logo" style="background:{{ contact.color }}"><sc-if value="{{ contact.hasLogo }}"><img data-dc-attr-src="{{ contact.logo }}" alt=""></sc-if><sc-if value="{{ contact.noLogo }}">{{ contact.initial }}</sc-if></span><strong>{{ contact.name }}</strong></span>
7099              <span class="ros-tv-contact-caption">Teléfonos / contactos del ticker</span><input type="text" data-dc-attr-value="{{ contact.value }}" aria-label="{{ contact.label }}" onInput="{{ contact.change }}" placeholder="+54 9 ... | +54 9 ...">
7100            </label></sc-for></div>
7101          </section>
7102        </div>
7103      </main>
7104    </sc-if>
7105
7106    <sc-if value="{{ isRosganTvLower }}" hint-placeholder-val="false">
7107      <main class="rosgan-tv-output {{ rosTvOutputClass }}" aria-label="Salida de TV del remate">
7108        <video data-rosgan-tv-multi-deck="1" muted loop playsinline preload="auto"></video>
7109        <video data-rosgan-tv-multi-deck="2" muted loop playsinline preload="auto"></video>
7110        <video data-rosgan-tv-multi-deck="3" muted loop playsinline preload="auto"></video>
7111        <video data-rosgan-tv-multi-deck="4" muted loop playsinline preload="auto"></video>
7112        <video data-rosgan-tv-multi-deck="5" muted loop playsinline preload="auto"></video>
7113        <video data-rosgan-tv-multi-deck="6" muted loop playsinline preload="auto"></video>
7114        <video data-rosgan-tv-multi-deck="7" muted loop playsinline preload="auto"></video>
7115        <video data-rosgan-tv-multi-deck="8" muted loop playsinline preload="auto"></video>
7116        <video data-rosgan-tv-deck="a" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video>
7117        <video data-rosgan-tv-deck="b" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video>
7118        <video data-rosgan-tv-deck="c" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video>
7119        <video data-rosgan-tv-deck="d" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video>
7120        <video data-rosgan-tv-deck="e" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video>
7121        <sc-if value="{{ rosPlateVisible }}"><section class="tv-waiting-screen rg-full-plate" aria-label="Placa del remate"><div class="tv-waiting-frame"><div class="tv-waiting-logos"><div class="tv-waiting-logo is-rosgan"><img src="/assets/rosgan-logo-blue.png" alt="Rosgan"></div><sc-for list="{{ rosPlateLogos }}" as="brand"><div class="tv-waiting-logo"><img data-dc-attr-src="{{ brand.logo }}" alt="{{ brand.name }}"></div></sc-for></div><sc-if value="{{ rosWaitActive }}"><div class="tv-waiting-body"><div class="tv-waiting-copy"><span class="tv-waiting-kicker">Transmisión oficial</span><h1 class="tv-waiting-title">{{ rosPlateTitle }}</h1><div class="tv-waiting-facts"><span class="tv-waiting-fact"><small>Fecha y hora</small><strong>{{ rosPlateDate }}</strong></span><span class="tv-waiting-fact"><small>Lugar</small><strong>{{ rosPlateLocation }}</strong></span></div></div><div class="tv-waiting-countdown"><rosgan-countdown start="{{ rosPlateStart }}"></rosgan-countdown></div></div></sc-if><sc-if value="{{ rosEndActive }}"><div class="tv-end-copy"><span class="tv-end-kicker">{{ rosPlateTitle }}</span><h1 class="tv-end-title">FIN DEL REMATE</h1><p class="tv-end-thanks">Gracias por acompañarnos</p></div></sc-if></div></section></sc-if>
7122        <section class="rtv-name-strap {{ rosNameStrapClass }}" aria-label="Presentación al aire" aria-hidden="{{ rosNameHidden }}"><div class="rtv-name-emblem"><img src="/assets/brand/rosgan-iso-blue.png" alt="Rosgan"></div><div class="rtv-name-copy"><strong class="rtv-name-title">{{ rosNameOnAirText }}</strong><sc-if value="{{ rosRoleOnAirText }}"><span class="rtv-name-subtitle">{{ rosRoleOnAirText }}</span></sc-if></div></section>
7123        <sc-if value="{{ rosTvShowSingleOverlay }}">
7124          <sc-if value="{{ rosTvShowBrandBug }}"><div class="rtv-brand"><rosgan-brand-bug name="{{ rosTvConsignataria }}" iso="{{ rosTvConsLogo }}" full="{{ rosTvConsFullLogo }}" style="--brand:{{ rosTvConsColor }};"></rosgan-brand-bug></div></sc-if>
7125          <sc-if value="{{ rosTvShowLower }}">
7126            <section class="rtv-lower" aria-label="Información del lote">
7127              <sc-if value="{{ rosTvShowChina }}"><div class="rtv-china-badge"><img src="/assets/icons/china-flag.svg" alt="" aria-hidden="true"><span>Apto China</span></div></sc-if>
7128              <sc-if value="{{ rosTvShowVisec }}"><div class="rtv-china-badge rtv-visec-badge"><img src="/assets/icons/visec-mark.svg" alt="" aria-hidden="true"><span>Apto VISEC</span></div></sc-if>
7129              <sc-if value="{{ rosTvHasObservations }}"><div class="rtv-observations"><span class="rtv-observations-label">OBSERVACIONES</span><div class="rtv-observations-window"><div class="rtv-observations-track" style="--roll-duration:{{ rosTvObservationsDuration }}s;"><span class="rtv-observations-copy">{{ rosTvObservations }}</span><span class="rtv-observations-copy" aria-hidden="true">{{ rosTvObservations }}</span></div></div></div></sc-if>
7130              <div class="rtv-panel">
7131                <div class="rtv-primary">
7132                  <sc-if value="{{ rosTvLotNumber }}"><div class="rtv-number"><small>LOTE</small><strong>{{ rosTvLotNumber }}</strong></div></sc-if>
7133                  <div class="rtv-identity"><div class="rtv-identity-fixed"><div class="rtv-certificate">CERTIFICADO {{ rosTvCert }}</div><h1 class="rtv-title">{{ rosTvCategory }}</h1><sc-if value="{{ rosTvBreed }}"><div class="rtv-breed">{{ rosTvBreed }}</div></sc-if></div><sc-if value="{{ rosTvHasExtraFields }}"><div class="rtv-rotating"><div class="rtv-rotating-label"><span>{{ rosTvExtraLabel }}</span></div><div class="rtv-rotating-value">{{ rosTvExtraValue }}</div></div></sc-if></div>
7134                  <div class="rtv-metrics"><sc-if value="{{ rosTvHasQuantity }}"><div class="rtv-metric"><small>CANTIDAD</small><strong class="rtv-quantity">{{ rosTvQuantity }}</strong><sc-if value="{{ rosTvQuantityFirst }}"><div class="rtv-quantity-breakdown"><span>{{ rosTvQuantityFirst }}</span><span>{{ rosTvQuantitySecond }}</span></div></sc-if></div></sc-if><sc-if value="{{ rosTvHasWeight }}"><div class="rtv-metric"><small>PESO PROMEDIO</small><strong>{{ rosTvWeight }}</strong></div></sc-if></div>
7135                </div>
7136                <div class="rtv-secondary"><sc-if value="{{ rosTvLocation }}"><div class="rtv-detail rtv-detail--location"><small>UBICACIÓN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><strong>{{ rosTvLocation }}</strong></div></sc-if><sc-if value="{{ rosTvPayment }}"><div class="rtv-detail rtv-detail--payment"><small>PLAZO DE PAGO</small><strong>{{ rosTvPayment }}</strong></div></sc-if><sc-if value="{{ rosTvTick }}"><div class="rtv-detail"><small>GARRAPATA</small><strong>{{ rosTvTick }}</strong></div></sc-if><sc-if value="{{ rosTvPregnancy }}"><div class="rtv-detail"><small>PREÑEZ</small><strong>{{ rosTvPregnancy }}</strong></div></sc-if></div>
7137              </div>
7138              <sc-if value="{{ rosTvShowContacts }}"><div class="rtv-contacts"><span class="rtv-contacts-label">CONTACTO COMERCIAL</span><span class="rtv-contacts-copy">{{ rosTvContacts }}</span></div></sc-if>
7139            </section>
7140          </sc-if>
7141        </sc-if>
7142<sc-for list="{{ rosTvAirPanes }}" as="pane"><div class="ros-tv-air-pane" style="{{ pane.style }}">          <sc-if value="{{ pane.ShowBrandBug }}"><div class="rtv-brand"><rosgan-brand-bug name="{{ pane.Consignataria }}" iso="{{ pane.ConsLogo }}" full="{{ pane.ConsFullLogo }}" style="--brand:{{ pane.ConsColor }};"></rosgan-brand-bug></div></sc-if>
7143          <sc-if value="{{ pane.ShowLower }}">
7144            <section class="rtv-lower" aria-label="Información del lote">
7145              <sc-if value="{{ pane.ShowChina }}"><div class="rtv-china-badge"><img src="/assets/icons/china-flag.svg" alt="" aria-hidden="true"><span>Apto China</span></div></sc-if>
7146              <sc-if value="{{ pane.ShowVisec }}"><div class="rtv-china-badge rtv-visec-badge"><img src="/assets/icons/visec-mark.svg" alt="" aria-hidden="true"><span>Apto VISEC</span></div></sc-if>
7147              <sc-if value="{{ pane.HasObservations }}"><div class="rtv-observations"><span class="rtv-observations-label">OBSERVACIONES</span><div class="rtv-observations-window"><div class="rtv-observations-track" style="--roll-duration:{{ pane.ObservationsDuration }}s;"><span class="rtv-observations-copy">{{ pane.Observations }}</span><span class="rtv-observations-copy" aria-hidden="true">{{ pane.Observations }}</span></div></div></div></sc-if>
7148              <div class="rtv-panel">
7149                <div class="rtv-primary">
7150                  <sc-if value="{{ pane.LotNumber }}"><div class="rtv-number"><small>LOTE</small><strong>{{ pane.LotNumber }}</strong></div></sc-if>
7151                  <div class="rtv-identity"><div class="rtv-identity-fixed"><div class="rtv-certificate">CERTIFICADO {{ pane.Cert }}</div><h1 class="rtv-title">{{ pane.Category }}</h1><sc-if value="{{ pane.Breed }}"><div class="rtv-breed">{{ pane.Breed }}</div></sc-if></div><sc-if value="{{ pane.HasExtraFields }}"><div class="rtv-rotating"><div class="rtv-rotating-label"><span>{{ pane.ExtraLabel }}</span></div><div class="rtv-rotating-value">{{ pane.ExtraValue }}</div></div></sc-if></div>
7152                  <div class="rtv-metrics"><sc-if value="{{ pane.HasQuantity }}"><div class="rtv-metric"><small>CANTIDAD</small><strong class="rtv-quantity">{{ pane.Quantity }}</strong><sc-if value="{{ pane.QuantityFirst }}"><div class="rtv-quantity-breakdown"><span>{{ pane.QuantityFirst }}</span><span>{{ pane.QuantitySecond }}</span></div></sc-if></div></sc-if><sc-if value="{{ pane.HasWeight }}"><div class="rtv-metric"><small>PESO PROMEDIO</small><strong>{{ pane.Weight }}</strong></div></sc-if></div>
7153                </div>
7154                <div class="rtv-secondary"><sc-if value="{{ pane.Location }}"><div class="rtv-detail rtv-detail--location"><small>UBICACIÓN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><strong>{{ pane.Location }}</strong></div></sc-if><sc-if value="{{ pane.Payment }}"><div class="rtv-detail rtv-detail--payment"><small>PLAZO DE PAGO</small><strong>{{ pane.Payment }}</strong></div></sc-if><sc-if value="{{ pane.Tick }}"><div class="rtv-detail"><small>GARRAPATA</small><strong>{{ pane.Tick }}</strong></div></sc-if><sc-if value="{{ pane.Pregnancy }}"><div class="rtv-detail"><small>PREÑEZ</small><strong>{{ pane.Pregnancy }}</strong></div></sc-if></div>
7155              </div>
7156              <sc-if value="{{ pane.ShowContacts }}"><div class="rtv-contacts"><span class="rtv-contacts-label">CONTACTO COMERCIAL</span><span class="rtv-contacts-copy">{{ pane.Contacts }}</span></div></sc-if>
7157            </section>
7158          </sc-if>
7159</div></sc-for>
7160      </main>
7161    </sc-if>
7162
7163    <sc-if value="{{ isCabanaCommercial }}" hint-placeholder-val="false">
7164      <main class="commercial-page">
7165        <div class="commercial-shell">
7166          <button class="commercial-back" onClick="{{ cbCommercialBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</button>
7167          <header class="commercial-hero">
7168            <div>
7169              <span class="commercial-kicker">CARGA COMERCIAL</span>
7170              <h1>Precios del remate</h1>
7171              <p>{{ cbCommercialTitle }}. Carga y actualizacion de valores por RP.</p>
7172            </div>
7173            <div class="commercial-hero-actions">
7174              <button class="commercial-prices-link" onClick="{{ cbCommercialOpenPrices }}"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="">Ver placa de precios</button>
7175              <span class="commercial-autosave-note"><img src="assets/icons/lucide/save.svg" alt="">Autoguardado</span>
7176            </div>
7177          </header>
7178          <section class="cb-installment-settings"><div><strong>Preoferta por cuota</strong><p>{{ cbInstallmentSummary }}</p></div><button class="commercial-prices-link" onClick="{{ cbEditInstallments }}">Configurar cuotas</button></section>
7179          <section class="commercial-summary" aria-label="Resumen comercial">
7180            <sc-for list="{{ cbCommercialMetrics }}" as="metric" hint-placeholder-count="4">
7181              <article class="commercial-metric">
7182                <span class="commercial-metric-icon" style="background:{{ metric.bg }};"><img data-dc-attr-src="{{ metric.iconUrl }}" alt=""></span>
7183                <span><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong><em>{{ metric.pct }}</em></span>
7184              </article>
7185            </sc-for>
7186          </section>
7187          <nav class="commercial-filters" aria-label="Filtrar por estado">
7188            <sc-for list="{{ cbCommercialFilters }}" as="filter" hint-placeholder-count="5">
7189              <button class="{{ filter.className }}" onClick="{{ filter.set }}" style="background:{{ filter.bg }}; border-color:{{ filter.border }}; color:{{ filter.color }};">{{ filter.label }} <b style="background:{{ filter.pillBg }};">{{ filter.count }}</b></button>
7190            </sc-for>
7191          </nav>
7192          <div class="commercial-view-tabs" role="tablist" aria-label="Vista comercial">
7193            <button class="{{ cbCommercialEntryTabClass }}" onClick="{{ cbCommercialShowEntry }}" role="tab" aria-selected="{{ cbCommercialIsEntry }}"><img src="assets/icons/lucide/pencil-line.svg" alt="">Carga de precios</button>
7194            <button class="{{ cbCommercialListTabClass }}" onClick="{{ cbCommercialShowList }}" role="tab" aria-selected="{{ cbCommercialIsList }}"><img src="assets/icons/lucide/table-2.svg" alt="">Listado y Excel</button>
7195          </div>
7196          <sc-if value="{{ cbCommercialIsEntry }}">
7197          <section class="commercial-board" aria-label="Carga de precios por RP">
7198            <div class="commercial-table-head">
7199              <span>Orden</span><span>{{ cbCommercialGroupLabel }}</span><span>RP</span><span>N° / Pintura</span><span>Categoria</span><span>Consig.</span><span>Estado</span><span>Precio</span><span></span>
7200            </div>
7201            <sc-if value="{{ cbCommercialLoading }}"><div class="commercial-empty"><span class="dc-spinner"></span> Cargando RPs...</div></sc-if>
7202            <sc-if value="{{ cbCommercialHasRows }}">
7203              <sc-for list="{{ cbCommercialRows }}" as="row" hint-placeholder-count="8">
7204                <sc-if value="{{ row.showCategoryDivider }}"><div class="commercial-category-divider">{{ row.categoryDividerTitle }}</div></sc-if>
7205                <article class="commercial-row">
7206                  <div class="commercial-row-main">
7207                    <div><small class="commercial-row-muted">Orden</small><div class="commercial-order-badge">{{ row.order }}</div></div>
7208                    <div><small class="commercial-row-muted">{{ row.groupLabel }}</small><div class="commercial-row-strong">{{ row.lotNumber }}</div></div>
7209                    <div><small class="commercial-row-muted">RP</small><div class="commercial-row-strong">{{ row.rp }}</div></div>
7210                    <div><small class="commercial-row-muted">N° / Pintura</small><div class="commercial-row-strong">{{ row.paintNumber }}</div></div>
7211                    <div class="commercial-mobile-span"><small class="commercial-row-muted">Categoria</small><div class="commercial-category">{{ row.category }}</div></div>
7212                    <div><span class="commercial-cons-avatar" style="background:{{ row.consColor }};"><sc-if value="{{ row.hasConsLogo }}"><img data-dc-attr-src="{{ row.consLogo }}" alt=""></sc-if><sc-if value="{{ row.noConsLogo }}"><strong>{{ row.consInitial }}</strong></sc-if></span></div>
7213                    <div class="commercial-status commercial-mobile-span">
7214                      <span class="commercial-status-pill" style="background:{{ row.statusBg }}; border-color:{{ row.statusBorder }}; color:{{ row.statusColor }};">{{ row.statusLabel }}</span>
7215                      <sc-for list="{{ row.statusOptions }}" as="status" hint-placeholder-count="4">
7216                        <button class="{{ status.className }}" onClick="{{ status.set }}">{{ status.label }}</button>
7217                      </sc-for>
7218                    </div>
7219                    <div class="commercial-price"><button class="cb-installment-edit" onClick="{{ row.editInstallments }}">{{ row.installmentLabel }}</button>
7220                      <div class="commercial-mode"><button class="{{ row.kiloModeClass }}" onClick="{{ row.setKilo }}" disabled="{{ row.priceDisabled }}">$ / Kilo</button><button class="{{ row.cabezaModeClass }}" onClick="{{ row.setCabeza }}" disabled="{{ row.priceDisabled }}">{{ row.headPriceLabel }}</button></div>
7221                      <input value="" data-dc-attr-value="{{ row.priceValue }}" onInput="{{ row.setPrice }}" onBlur="{{ row.commitPrice }}" onKeyDown="{{ row.commitPriceEnter }}" disabled="{{ row.priceDisabled }}" inputmode="numeric" placeholder="{{ row.pricePlaceholder }}"><small>{{ row.installmentTotal }}</small>
7222                    </div>
7223                    <button class="commercial-more-button" onClick="{{ row.toggleMore }}" title="Mas info" aria-label="Mas info"><img data-dc-attr-src="{{ row.moreIcon }}" alt=""></button>
7224                  </div>
7225                    <div class="{{ row.moreClassName }}">
7226                      <div class="commercial-status commercial-more-status">
7227                        <sc-for list="{{ row.statusOptions }}" as="status" hint-placeholder-count="4">
7228                          <button class="{{ status.className }}" onClick="{{ status.set }}">{{ status.label }}</button>
7229                        </sc-for>
7230                      </div>
7231                      <div class="commercial-more-grid">
7232                        <label>Cantidad<input disabled="{{ row.hasDivisions }}" value="" data-dc-attr-value="{{ row.cantidad }}" onInput="{{ row.setCantidad }}" inputmode="numeric" placeholder="Cantidad"></label>
7233                        <label>Peso prom.<input value="" data-dc-attr-value="{{ row.pesoProm }}" onInput="{{ row.setPesoProm }}" inputmode="decimal" placeholder="Kg"></label>
7234                        <label>Plazo<input value="" data-dc-attr-value="{{ row.plazo }}" onInput="{{ row.setPlazo }}" placeholder="Ej: 30 dias"></label>
7235                        <label>Comprador<input disabled="{{ row.hasDivisions }}" value="" data-dc-attr-value="{{ row.comprador }}" onInput="{{ row.setComprador }}" placeholder="Nombre o razon social"></label>
7236                      </div>
7237                      <button class="commercial-prices-link" onClick="{{ row.divideSale }}">{{ row.divisionLabel }}</button>
7238                    </div>
7239                </article>
7240              </sc-for>
7241            </sc-if>
7242            <sc-if value="{{ cbCommercialEmpty }}"><div class="commercial-empty">No hay RPs para este filtro.</div></sc-if>
7243          </section>
7244          </sc-if>
7245          <sc-if value="{{ cbCommercialIsList }}">
7246            <section class="commercial-report" aria-label="Listado de resultados del remate">
7247              <header class="commercial-report-head">
7248                <div><h2>Listado de resultados</h2><p>Lotes y reproductores del remate con identificación, estado y precio cargado.</p></div>
7249                <button class="commercial-export-excel" onClick="{{ cbCommercialExportExcel }}"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Exportar Excel</button>
7250              </header>
7251              <sc-if value="{{ cbCommercialLoading }}"><div class="commercial-empty"><span class="dc-spinner"></span> Preparando listado...</div></sc-if>
7252              <sc-if value="{{ cbCommercialHasReportRows }}">
7253                <div class="commercial-report-scroll">
7254                  <table class="commercial-report-table">
7255                    <thead><tr><th>Orden</th><th>{{ cbCommercialGroupLabel }}</th><th>RP</th><th>N° / Pintura</th><th>Categoría</th><th>Cabaña</th><th>Consignataria</th><th>Cantidad</th><th>Estado</th><th>
7255Modalidad</th><th style="text-align:right;">Precio</th><th style="text-align:right;">X / Total</th></tr></thead>
7256                    <tbody>
7257                      <sc-for list="{{ cbCommercialReportRows }}" as="row" hint-placeholder-count="8">
7258                        <tr>
7259                          <td class="is-strong" data-label="Orden">{{ row.order }}</td><td data-dc-attr-data-label="{{ row.groupLabel }}">{{ row.lotNumber }}</td><td class="is-strong" data-label="RP">{{ row.rp }}</td><td data-label="N° / Pintura">{{ row.paintNumber }}</td><td data-label="Categoría">{{ row.category }}</td><td data-label="Cabaña">{{ row.cabana }}</td><td data-label="Consignataria">{{ row.consignataria }}</td><td class="is-number" data-label="Cantidad">{{ row.cantidad }}</td>
7260                          <td data-label="Estado"><span class="commercial-report-status" style="background:{{ row.statusBg }}; border-color:{{ row.statusBorder }}; color:{{ row.statusColor }};">{{ row.statusLabel }}</span></td><td class="is-muted" data-label="Modalidad">{{ row.priceUnitLabel }}</td><td class="is-number is-strong" data-label="Precio">{{ row.priceDisplay }}</td><td class="is-number is-strong" data-label="X / Total">{{ row.totalDisplay }}</td>
7261                        </tr>
7262                      </sc-for>
7263                    </tbody>
7264                  </table>
7265                </div>
7266                <footer class="commercial-report-foot"><span>{{ cbCommercialReportCountLabel }}</span><span>El Excel incluye también peso promedio, plazo y comprador cuando están cargados.</span></footer>
7267              </sc-if>
7268              <sc-if value="{{ cbCommercialReportEmpty }}"><div class="commercial-empty">No hay lotes o RPs para mostrar.</div></sc-if>
7269            </section>
7270          </sc-if>
7271        </div>
7272      </main>
7273    </sc-if>
7274    <sc-if value="{{ isCabanaPriceBoard }}" hint-placeholder-val="false">
7275      <main class="price-board-page">
7276        <div class="price-board-shell">
7277          <button class="commercial-back" onClick="{{ pbBackCatalog }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</button>
7278          <div class="price-board-toolbar">
7279            <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;"><button class="commercial-prices-link price-board-story-export" onClick="{{ pbExportStory }}"><img src="assets/icons/brands/instagram.svg" alt="">{{ pbStoryLabel }}</button><div class="price-board-updated">{{ pbUpdatedLabel }}</div></div>
7280          </div>
7281          <rosgan-price-board data-board="{{ pbBoardData }}"></rosgan-price-board>
7282        </div>
7283      </main>
7284    </sc-if>
7285    <sc-if value="{{ isCabanaTvConfig }}" hint-placeholder-val="false">
7286      <main class="tv-config-page">
7287        <div class="tv-config-shell">
7288          <button class="tv-config-back" onClick="{{ tvConfigBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</button>
7289          <header class="tv-config-hero">
7290            <div>
7291              <small>Control TV</small>
7292              <h1>Zocalo del remate</h1>
7293              <p>Seleccion de lote y campos visibles. Los cambios se reflejan en tiempo real en la salida /tv.</p>
7294            </div>
7295            <div class="tv-config-live">
7296              <span>{{ tvVisibleLabel }}</span>
7297              <button class="{{ tvVisibleSwitchClass }}" onClick="{{ tvToggleVisible }}" aria-label="Mostrar u ocultar toda la grafica"><span></span></button>
7298            </div>
7299          </header>
7300          <section class="tv-config-panel">
7301            <div class="tv-config-toolbar">
7302              <button onClick="{{ tvPrev }}">Anterior</button>
7303              <select value="" data-dc-attr-value="{{ tvSelectedKey }}" onChange="{{ tvSelectLot }}">
7304                <sc-for list="{{ tvLotOptions }}" as="opt" hint-placeholder-count="8">
7305                  <option value="" data-dc-attr-value="{{ opt.key }}">{{ opt.label }}</option>
7306                </sc-for>
7307              </select>
7308              <button onClick="{{ tvNext }}">Siguiente</button>
7309            </div>
7310            <div class="tv-config-fields">
7311              <div class="tv-config-side">
7312                <article class="tv-config-card">
7313                  <div class="tv-config-card-head">
7314                    <h2>Elementos de pantalla</h2>
7315                  </div>
7316                  <div class="tv-config-screen-elements">
7317                    <sc-for list="{{ tvScreenElements }}" as="item" hint-placeholder-count="4">
7318                      <button class="{{ item.className }}" onClick="{{ item.toggle }}" disabled="{{ item.disabled }}" type="button"><span>{{ item.label }}</span><i><b></b></i></button>
7319                    </sc-for>
7320                  </div>
7321                  <label class="tv-config-contacts">Teléfonos del ticker
7322                    <textarea data-dc-attr-value="{{ tvContactsText }}" onChange="{{ tvContactsChange }}" maxlength="2000" placeholder="Nombre · teléfono | Nombre · teléfono" aria-label="Teléfonos del ticker"></textarea>
7323                    <small>Separá los contactos con |. Se guardan automáticamente y se muestran en la salida TV.</small>
7324                  </label>
7325                  <sc-if value="{{ tvVideosConfigVisible }}" hint-placeholder-val="false">
7326                    <div class="tv-config-video-mode">
7327                      <sc-for list="{{ tvVideoModeOptions }}" as="mode" hint-placeholder-count="2">
7328                        <button class="{{ mode.className }}" onClick="{{ mode.select }}" type="button">{{ mode.label }}</button>
7329                      </sc-for>
7330                    </div>
7331                    <sc-if value="{{ tvVideoTransportVisible }}" hint-placeholder-val="false">
7332                      <div class="tv-config-transport" aria-label="Control de reproduccion por RP">
7333                        <div class="tv-config-transport-head">
7334                          <span><small>Reproduccion por RP</small><strong>{{ tvVideoTransportRp }}</strong></span>
7335                          <em>{{ tvVideoPlaybackStatus }}</em>
7336                        </div>
7337                        <select value="" data-dc-attr-value="{{ tvVideoSelectedKey }}" onChange="{{ tvSelectVideoRp }}" disabled="{{ tvVideoTransportDisabled }}">
7338                          <sc-for list="{{ tvVideoRpOptions }}" as="video" hint-placeholder-count="4">
7339                            <option value="" data-dc-attr-value="{{ video.key }}">{{ video.label }}</option>
7340                          </sc-for>
7341                        </select>
7342                        <div class="tv-config-transport-controls">
7343                          <button onClick="{{ tvPrevVideoRp }}" disabled="{{ tvVideoTransportDisabled }}" type="button">RP anterior</button>
7344                          <button onClick="{{ tvRestartVideo }}" disabled="{{ tvVideoTransportDisabled }}" type="button">Reiniciar</button>
7345                          <button class="is-primary" onClick="{{ tvToggleVideoPlayback }}" disabled="{{ tvVideoTransportDisabled }}" type="button">{{ tvVideoPlaybackAction }}</button>
7346                          <button onClick="{{ tvNextVideoRp }}" disabled="{{ tvVideoTransportDisabled }}" type="button">RP siguiente</button>
7347                        </div>
7348                      </div>
7349                    </sc-if>
7350                  </sc-if>
7351                </article>
7352                <article class="tv-config-card">
7353                <h2>{{ tvGroupListTitle }}</h2>
7354                  <div class="tv-config-list">
7355                    <sc-for list="{{ tvLotRows }}" as="lot" hint-placeholder-count="8">
7356                     <button class="{{ lot.className }}" onClick="{{ lot.select }}" disabled="{{ lot.disabled }}"><strong>{{ lot.lotNumber }}</strong><span>{{ lot.category }}</span><small>{{ lot.count }} RP</small><i class="{{ lot.statusClass }}" aria-label="{{ lot.statusLabel }}"><sc-if value="{{ lot.statusIsLifted }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-minus" aria-hidden="true"></i></sc-if><sc-if value="{{ lot.statusIsResolved }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if></i></button>
7357                    </sc-for>
7358                  </div>
7359                </article>
7360              </div>
7361              <article class="tv-config-card">
7362                <div class="tv-config-card-head">
7363                  <h2>Campos por categoria</h2>
7364                  <div class="tv-config-mini-toggle">
7365                    <span>{{ tvDepsLabel }}</span>
7366                    <button class="{{ tvDepsSwitchClass }}" onClick="{{ tvToggleDeps }}" aria-label="Activar DEPs y Precision"><span></span></button>
7367                  </div>
7368                </div>
7369                <div class="tv-config-category-fields">
7370                  <sc-for list="{{ tvCategoryFieldGroups }}" as="group" hint-placeholder-count="4">
7371                    <section class="tv-config-category-card">
7372                      <h3>{{ group.label }}</h3>
7373                      <div class="tv-config-subforms">
7374                        <sc-for list="{{ group.subforms }}" as="subform" hint-placeholder-count="3">
7375                          <div class="tv-config-subform">
7376                            <h4>{{ subform.label }}</h4>
7377                            <div class="tv-config-checks">
7378                              <sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="5">
7379                                <label class="tv-config-check"><input type="checkbox" checked="{{ field.checked }}" onChange="{{ field.toggle }}">{{ field.label }}</label>
7380                              </sc-for>
7381                            </div>
7382                          </div>
7383                        </sc-for>
7384                      </div>
7385                    </section>
7386                  </sc-for>
7387                </div>
7388              </article>
7389            </div>
7390          </section>
7391        </div>
7392      </main>
7393    </sc-if>
7394    <sc-if value="{{ isCabanaTvLower }}" hint-placeholder-val="false">
7395      <main class="tv-lower-page">
7396        <div class="tv-lower-stage">
7397          <sc-if value="{{ tvShowControls }}">
7398            <div class="tv-lower-control">
7399              <button onClick="{{ tvPrev }}">Anterior</button>
7400              <select value="" data-dc-attr-value="{{ tvSelectedKey }}" onChange="{{ tvSelectLot }}">
7401                <sc-for list="{{ tvLotOptions }}" as="opt" hint-placeholder-count="8">
7402                  <option value="" data-dc-attr-value="{{ opt.key }}">{{ opt.label }}</option>
7403                </sc-for>
7404              </select>
7405              <button onClick="{{ tvNext }}">Siguiente</button>
7406            </div>
7407          </sc-if>
7408          <sc-if value="{{ tvHasLot }}">
7409            <sc-if value="{{ tvChannelBugVisible }}">
7410              <div class="tv-channel-bug" style="--tv-cons-color:{{ tvConsColor }};
7410--tv-cabana-color:{{ tvCabanaColor }};--tv-cabana-logo-filter:{{ tvCabanaLogoFilter }};--tv-cabana-logo-background:{{ tvCabanaLogoBackground }};">
7411                <sc-if value="{{ tvLogosVisible }}">
7412                  <div class="tv-bug-strip">
7413                    <div class="tv-bug-part tv-bug-rosgan"><img class="tv-bug-logo" data-dc-attr-src="{{ tvRosganLogo }}" alt="Rosgan"></div>
7414                    <div class="tv-bug-part tv-bug-cons"><sc-if value="{{ tvHasConsLogo }}"><img class="tv-bug-logo" data-dc-attr-src="{{ tvConsLogo }}" alt=""></sc-if><sc-if value="{{ tvNoConsLogo }}"><strong>{{ tvConsInitial }}</strong></sc-if><span class="tv-bug-cons-name">{{ tvConsName }}</span></div>
7415                    <div class="tv-bug-part tv-bug-cabana"><sc-if value="{{ tvHasLogo }}"><img class="tv-bug-logo" data-dc-attr-src="{{ tvLogo }}" alt="{{ tvCabanaName }}" onError="{{ tvLogoError }}"></sc-if><sc-if value="{{ tvNoLogo }}"><strong>{{ tvCabanaName }}</strong></sc-if></div>
7416                  </div>
7417                </sc-if>
7418                <sc-if value="{{ tvLiveBugVisible }}">
7419                  <div class="tv-bug-live"><sc-if value="{{ tvClockVisible }}"><span class="tv-bug-clock"><img src="assets/icons/lucide/clock-3.svg" alt="" aria-hidden="true">{{ tvClockLabel }}</span></sc-if><sc-if value="{{ tvKickerVisible }}"><span class="tv-bug-live-card"><b>Vivo</b><span>{{ tvKicker }}</span></span></sc-if></div>
7420                </sc-if>
7421              </div>
7422            </sc-if>
7423            <sc-if value="{{ tvWeatherVisible }}">
7424              <div class="tv-weather-bug">
7425                <img class="tv-weather-icon" data-dc-attr-src="{{ tvWeatherIcon }}" alt="">
7426                <span class="tv-weather-temp">{{ tvWeatherTemp }}</span>
7427                <span class="tv-weather-copy"><strong>{{ tvWeatherLabel }}</strong><small>{{ tvWeatherLocation }}</small></span>
7428              </div>
7429            </sc-if>
7430            <sc-if value="{{ tvWaitingVisible }}">
7431              <section class="tv-waiting-screen tv-waiting-broadcast" style="--tv-wait-color:{{ tvConsColor }};" aria-label="Placa de espera del remate">
7432                <span class="tv-waiting-orb is-one"></span>
7433                <span class="tv-waiting-orb is-two"></span>
7434                <div class="tv-waiting-frame">
7435                  <div class="tv-waiting-logos">
7436                    <div class="tv-waiting-logo is-rosgan"><img data-dc-attr-src="{{ tvRosganLogo }}" alt="Rosgan"></div>
7437                    <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvWaitingConsLogoFilter }};">
7438                      <sc-if value="{{ tvHasConsLogo }}"><img data-dc-attr-src="{{ tvConsLogo }}" alt="{{ tvConsName }}"></sc-if>
7439                      <sc-if value="{{ tvNoConsLogo }}"><strong>{{ tvConsName }}</strong></sc-if>
7440                    </div>
7441                    <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvCabanaLogoFilter }};">
7442                      <sc-if value="{{ tvHasLogo }}"><img data-dc-attr-src="{{ tvLogo }}" alt="{{ tvCabanaName }}" onError="{{ tvLogoError }}"></sc-if>
7443                      <sc-if value="{{ tvNoLogo }}"><strong>{{ tvCabanaName }}</strong></sc-if>
7444                    </div>
7445                  </div>
7446                  <div class="tv-waiting-body">
7447                    <div class="tv-waiting-copy">
7448                      <span class="tv-waiting-kicker">Transmisión oficial</span>
7449                      <h1 class="tv-waiting-title">{{ tvWaitingTitle }}</h1>
7450                      <div class="tv-waiting-facts">
7451                        <span class="tv-waiting-fact"><small>Fecha</small><strong>{{ tvWaitingDate }}</strong></span>
7452                        <span class="tv-waiting-fact"><small>Hora</small><strong>{{ tvWaitingTime }}</strong></span>
7453                        <span class="tv-waiting-fact is-location"><small>Localidad</small><strong>{{ tvWaitingLocation }}</strong></span>
7454                      </div>
7455                    </div>
7456                    <div class="tv-waiting-countdown">
7457                      <span class="tv-waiting-countdown-label">{{ tvWaitingCountdownLabel }}</span>
7458                      <sc-if value="{{ tvWaitingCountdownActive }}">
7459                        <div class="tv-waiting-count-grid">
7460                          <sc-for list="{{ tvWaitingCountdownParts }}" as="part" hint-placeholder-count="4">
7461                            <span class="tv-waiting-count-part"><strong>{{ part.value }}</strong><small>{{ part.label }}</small></span>
7462                          </sc-for>
7463                        </div>
7464                      </sc-if>
7465                      <sc-if value="{{ tvWaitingMessageVisible }}"><strong class="tv-waiting-message">{{ tvWaitingMessage }}</strong></sc-if>
7466                      <span class="tv-waiting-local-time">Hora Argentina <strong>{{ tvWaitingNow }}</strong></span>
7467                    </div>
7468                  </div>
7469                  <footer class="tv-waiting-broadcast-footer"><span><i></i> Transmisión oficial · Rosgan</span></footer>
7470                </div>
7471              </section>
7472            </sc-if>
7473            <sc-if value="{{ tvEndVisible }}">
7474              <section class="tv-waiting-screen tv-end-screen" style="--tv-wait-color:{{ tvConsColor }};" aria-label="Placa de fin del remate">
7475                <span class="tv-waiting-orb is-one"></span>
7476                <span class="tv-waiting-orb is-two"></span>
7477                <div class="tv-waiting-frame tv-end-frame">
7478                  <div class="tv-waiting-logos tv-end-logos">
7479                    <div class="tv-waiting-logo is-rosgan"><img data-dc-attr-src="{{ tvRosganLogo }}" alt="Rosgan"></div>
7480                    <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvWaitingConsLogoFilter }};">
7481                      <sc-if value="{{ tvHasConsLogo }}"><img data-dc-attr-src="{{ tvConsLogo }}" alt="{{ tvConsName }}"></sc-if>
7482                      <sc-if value="{{ tvNoConsLogo }}"><strong>{{ tvConsName }}</strong></sc-if>
7483                    </div>
7484                    <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvCabanaLogoFilter }};">
7485                      <sc-if value="{{ tvHasLogo }}"><img data-dc-attr-src="{{ tvLogo }}" alt="{{ tvCabanaName }}" onError="{{ tvLogoError }}"></sc-if>
7486                      <sc-if value="{{ tvNoLogo }}"><strong>{{ tvCabanaName }}</strong></sc-if>
7487                    </div>
7488                  </div>
7489                  <div class="tv-end-copy">
7490                    <span class="tv-end-kicker">Transmisión oficial</span>
7491                    <h1 class="tv-end-title">FIN DEL REMATE</h1>
7492                    <p class="tv-end-thanks">Gracias por acompañarnos</p>
7493                  </div>
7494                </div>
7495              </section>
7496            </sc-if>
7497            <sc-if value="{{ tvLowerThirdVisible }}">
7498              <section class="{{ tvLowerClass }}">
7499                <div class="tv-lower-topline">
7500                  <div class="tv-lower-title-line"><strong><span class="{{ tvCategoryRollClass }}">{{ tvCategory }}</span><span class="tv-title-count"><span class="tv-title-dot">&middot;</span> Cantidad: <b class="{{ tvCountRollClass }}">{{ tvCount }}</b></span><sc-if value="{{ tvShowingDeps }}" hint-placeholder-val="false"><span class="tv-deps-pill">{{ tvDepsModeLabel }}</span></sc-if></strong></div>
7501                  <div class="tv-lower-meta"></div>
7502                </div>
7503                <div class="tv-lower-lot-panel">
7504                  <div class="tv-lower-logo"><strong>{{ tvCabanaName }}</strong></div>
7505                  <div class="tv-lower-lot"><span class="{{ tvLotRollClass }}">{{ tvLotLabel }}</span><sc-if value="{{ tvHasActiveRp }}" hint-placeholder-val="false"><small class="{{ tvActiveRpRollClass }}">{{ tvActiveRpLabel }}</small></sc-if></div>
7506                </div>
7507                <div class="{{ tvTableClass }}" style="--tv-cols:{{ tvGridCols }};">
7508                  <sc-if value="{{ tvShowTableHead }}" hint-placeholder-val="true">
7509                    <div class="tv-lower-row tv-lower-head">
7510                      <sc-for list="{{ tvColumns }}" as="col" hint-placeholder-count="6">
7511                        <span>{{ col.label }}</span>
7512                      </sc-for>
7513                    </div>
7514                  </sc-if>
7515                  <sc-for list="{{ tvRows }}" as="row" hint-placeholder-count="3">
7516                    <div class="{{ row.className }}">
7517                      <sc-for list="{{ row.cells }}" as="cell" hint-placeholder-count="6">
7518                        <span class="{{ cell.className }}"><i class="{{ cell.rollClass }}">{{ cell.value }}</i></span>
7519                      </sc-for>
7520                    </div>
7521                  </sc-for>
7522                </div>
7523              </section>
7524            </sc-if>
7525          </sc-if>
7526          <sc-if value="{{ tvShowContacts }}"><aside class="tv-phone-ticker" aria-label="Contacto comercial"><strong>CONTACTO COMERCIAL</strong><div class="tv-phone-ticker-window"><div class="tv-phone-ticker-track"><span>{{ tvContactsText }}</span><span aria-hidden="true">{{ tvContactsText }}</span></div></div></aside></sc-if>
7527          <sc-if value="{{ tvEmpty }}"><div class="tv-lower-empty">Cargando zocalo...</div></sc-if>
7528        </div>
7529      </main>
7530    </sc-if>
7531    <sc-if value="{{ isRosganSalesOrder }}" hint-placeholder-val="false">
7532      <main class="sales-order-page">
7533        <div class="sales-order-shell">
7534          <button class="sales-order-back" onClick="{{ closeRgSalesOrder }}"><img src="assets/icons/lucide/arrow-left.svg" alt="" width="15" height="15"> Volver al remate</button>
7535          <header class="sales-order-head">
7536            <div>
7537              <span class="sales-order-kicker">ADMINISTRACIÓN DEL REMATE</span>
7538              <h1>Orden de venta</h1>
7539              <p>{{ rgSalesOrderTitle }}</p>
7540            </div>
7541            <div class="sales-order-head-actions rg-order-save">              <button class="sales-order-action primary" onClick="{{ saveRgSalesOrder }}" style="opacity:{{ rgSalesOrderSaveOpacity }};">
7542                <img src="assets/icons/lucide/save.svg" alt="">
7543                {{ rgSalesOrderSaveLabel }}
7544              </button>
7545</div>
7546          </header>
7547          <div class="rg-order-tools" aria-label="Herramientas del orden de venta">
7548            <section class="rg-order-tool-group"><h2>Excel de trabajo</h2><p>Descargá el preorden o importá el orden preparado.</p><div class="rg-order-tool-buttons">              <button class="sales-order-action" onClick="{{ exportRgPreorderExcel }}" disabled="{{ rgPreorderDisabled }}" title="Descargar preorden completo de la consignataria seleccionada, ordenado por categoría y peso"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">{{ rgPreorderLabel }}</button>
7549              <label class="sales-order-action sales-order-import {{ rgSalesOrderImportClass }}">
7550                <img src="assets/icons/lucide/file-spreadsheet.svg" alt="">
7551                {{ rgSalesOrderImportLabel }}
7552                <input type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" onChange="{{ importRgSalesOrderExcel }}" aria-label="Cargar Excel con el orden de venta">
7553              </label>
7554</div></section>
7555            <section class="rg-order-tool-group"><h2>Catálogo</h2><p>Controlá la publicación y prepará sus páginas.</p><div class="rg-order-tool-buttons">              <button class="sales-order-action" role="switch" aria-label="Publicar Catálogo" aria-checked="{{ rgCatalogPublished }}" aria-disabled="{{ rgCatalogBusy }}" onClick="{{ toggleRgCatalog }}" style="opacity:{{ rgCatalogOpacity }};">
7556                <span style="width:34px;height:20px;border-radius:20px;background:{{ rgCatalogSwitchColor }};padding:3px;box-sizing:border-box;display:flex;align-items:center;"><span style="display:block;width:14px;height:14px;border-radius:50%;background:white;transform:translateX({{ rgCatalogSwitchOffset }}px);"></span></span>
7557                Publicar Catálogo
7558              </button>
7559</div><details class="rg-order-catalog-settings"><summary>Diseño del catálogo <span aria-hidden="true">⌄</span></summary><div class="rg-order-tool-buttons">              <sc-if value="{{ rgCanToggleLotLoading }}"><label class="sales-order-action sales-order-import"><img src="assets/icons/lucide/image.svg" alt="">Cargar tapa<input type="file" accept="image/jpeg,image/png" onChange="{{ uploadRgCatalogCover }}" aria-label="Cargar tapa del catálogo"></label></sc-if>
7560              <sc-if value="{{ rgCanToggleLotLoading }}">
7560<button class="sales-order-action" onClick="{{ configureRgCatalogNews }}">Páginas de noticias</button></sc-if>
7561              <sc-if value="{{ rgCanToggleLotLoading }}"><label class="sales-order-action sales-order-import"><img src="assets/icons/lucide/image.svg" alt="">Cargar contratapa<input type="file" accept="image/jpeg,image/png" onChange="{{ uploadRgCatalogBackCover }}" aria-label="Cargar contratapa del catálogo"></label></sc-if>
7562</div></details></section>
7563            <sc-if value="{{ rgCanToggleLotLoading }}"><section class="rg-order-tool-group"><h2>Carga de lotes</h2><p>Permití o cerrá el ingreso de nuevos lotes.</p><div class="rg-order-tool-buttons">              <sc-if value="{{ rgCanToggleLotLoading }}"><button type="button" class="sales-order-action lot-loading-switch" role="switch" aria-label="Habilitar carga de lotes" aria-checked="{{ rgLotLoadingEnabled }}" disabled="{{ rgLotLoadingBusy }}" onClick="{{ toggleRgLotLoading }}"><span class="lot-loading-track" aria-hidden="true"></span>{{ rgLotLoadingLabel }}</button></sc-if>
7564</div></section></sc-if>
7565          </div>
7566          <div class="sales-order-summary"><strong>{{ rgSalesOrderCount }}</strong> lotes. Usá las flechas o elegí una posición para definir el orden visible en el catálogo.</div>
7567          <sc-if value="{{ rgSalesOrderHasImport }}"><div class="sales-order-import-summary"><strong>{{ rgSalesOrderImportFile }}</strong><span>Inicio {{ rgSalesOrderImportStart }}</span><span>{{ rgSalesOrderImportGroups }} grupos</span><span>Fin estimado {{ rgSalesOrderImportEnd }}</span><small>Los horarios se recalculan con el promedio de cada alias.</small></div></sc-if>
7568          <div class="sales-order-filterbar">
7569            <label class="sales-order-search"><img src="assets/icons/lucide/search.svg" alt=""><input type="search" placeholder="Buscar certificado, posición, categoría, localidad, raza…" value="{{ rgSalesOrderQuery }}" onInput="{{ setRgSalesOrderQuery }}" aria-label="Buscar lotes del orden de venta"></label>
7570            <select value="{{ rgSalesOrderStatusFilter }}" onChange="{{ setRgSalesOrderStatusFilter }}" aria-label="Filtrar por estado"><sc-for list="{{ rgSalesOrderStatusOptions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for></select>
7571            <select value="{{ rgSalesOrderCategoryFilter }}" onChange="{{ setRgSalesOrderCategoryFilter }}
7571" aria-label="Filtrar por categoría"><sc-for list="{{ rgSalesOrderCategoryOptions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for></select>
7572            <select value="{{ rgSalesOrderConsignatariaFilter }}" onChange="{{ setRgSalesOrderConsignatariaFilter }}" aria-label="Filtrar por consignataria"><sc-for list="{{ rgSalesOrderConsignatariaOptions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for></select>
7573            <select value="{{ rgSalesOrderRatingFilter }}" onChange="{{ setRgSalesOrderRatingFilter }}" aria-label="Filtrar por calificación"><option value="">Todas las calificaciones</option><option value="unrated">Sin calificar</option><option value="rated">Calificados</option></select>
7574            <div class="sales-order-filter-count sales-order-progress" role="status"><span><strong>{{ rgSalesOrderCount }}</strong> lotes totales</span><span class="sales-order-pending"><strong>{{ rgSalesOrderUnratedCount }}</strong> sin calificar</span><span><strong>{{ rgSalesOrderVisibleCount }}</strong> visibles con estos filtros</span></div>
7575          </div>
7576          <div class="sales-order-command" style="background:#fff;border:1px solid #d8e4f0;border-radius:12px;padding:16px;margin-bottom:16px">
7577            <label for="rg-order-command" style="display:block;font-weight:800;margin-bottom:8px">Ordenar con una indicación</label>
7578            <div style="display:flex;gap:10px;flex-wrap:wrap"><input id="rg-order-command" type="text" class="sales-order-select" style="flex:1;min-width:220px;width:auto" placeholder="Quiero el lote 154 después del 4" onKeyDown="{{ rgOrderCommandEnter }}"><button class="sales-order-action" onClick="{{ rgOrderCommandApply }}">Aplicar</button></div>
7579            <p style="font-size:12px;color:#54718c;margin:8px 0 0">También: «lote 23 entre el 56 y el 57» o «lote 8 antes del 2». Usá los números de lote.</p>
7580            <div role="status" style="font-size:13px;color:#08745a;margin-top:8px">{{ rgOrderCommandMessage }}</div>
7581          </div>
7582          <sc-if value="{{ rgSalesOrderSaved }}"><div class="sales-order-message is-success">Orden guardado correctamente.</div></sc-if>
7583          <sc-if value="{{ rgSalesOrderHasError }}"><div class="sales-order-message is-error">{{ rgSalesOrderError }}</div></sc-if>
7584          <sc-if value="{{ rgSalesOrderLoading }}"><div class="sales-order-empty"><span class="dc-spinner"></span> Cargando lotes...</div></sc-if>
7585          <div class="sales-order-list">
7586            <sc-for list="{{ rgSalesOrderRows }}" as="row" hint-placeholder-count="8">
7587              <article class="sales-order-row">
7588                <span class="sales-order-position">{{ row.position }}</span>
7589                <div class="sales-order-lot-main">
7590                  <div class="sales-order-copy"><strong>{{ row.category }}</strong><span class="sales-order-status {{ row.statusClass }}">{{ row.statusLabel }}</span><sc-if value="{{ row.isOutOfCatalog }}"><small>Fuera de catálogo</small></sc-if><small>Cert. {{ row.cert }}<sc-if value="{{ row.breed }}"> · {{ row.breed }}</sc-if></small></div>
7591                  <div class="sales-order-facts"><sc-for list="{{ row.facts }}" as="fact"><span><small>{{ fact.label }}</small><strong>{{ fact.value }}</strong></span></sc-for></div>
7592                  <sc-if value="{{ row.hasExcelSchedule }}"><div class="sales-order-excel-data"><span><small>GRUPO / ALIAS DEL EXCEL</small><strong>{{ row.excelAlias }}</strong></span><span><small>HORA ESTIMADA</small><strong>{{ row.estimatedTime }}</strong></span><span><small>PROMEDIO POR LOTE</small><strong>{{ row.averageTime }}</strong></span></div></sc-if>
7593                  <div class="sales-order-lot-tools">
7594                    <span class="sales-order-consignataria"><span class="sales-order-consignataria-logo" style="background:{{ row.consignatariaLogoBg }};"><sc-if value="{{ row.hasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ row.consignatariaLogo }}" alt="{{ row.consignataria }}"></sc-if><sc-if value="{{ row.noConsignatariaLogo }}" hint-placeholder-val="false"><strong style="color:#fff;font-size:14px;">{{ row.consignatariaInitial }}</strong></sc-if></span><span><small>CONSIGNATARIA</small><strong>{{ row.consignataria }}</strong></span></span>
7595                    <div class="sales-order-rating" aria-label="{{ row.ratingLabel }}">
7596                      <span class="sales-order-rating-label" style="color:{{ row.ratingColor }};">{{ row.ratingLabel }}</span>
7597                      <span class="sales-order-stars"><sc-for list="{{ row.ratingStars }}" as="star"><button type="button" onClick="{{ star.pick }}" aria-label="{{ star.label }}" title="{{ star.label }}" style="color:{{ star.color }};">★</button></sc-for></span>
7598                    </div>
7599                    <sc-if value="{{ row.hasVideo }}" hint-placeholder-val="true"><button type="button" class="sales-order-video-button" onClick="{{ row.toggleVideo }}"><span class="sales-order-video-badge {{ row.videoBadgeClass }}">{{ row.videoLabel }}</span>{{ row.videoButtonLabel }}</button></sc-if>
7600                    <sc-if value="{{ row.noVideo }}" hint-placeholder-val="false"><span class="sales-order-no-video">Sin video disponible</span></sc-if>
7601                  </div>
7602                </div>
7603                <div class="sales-order-controls">
7604                  <button class="sales-order-control" onClick="{{ row.moveUp }}" disabled="{{ row.isFirst }}" title="Subir lote" aria-label="Subir lote"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button>
7605                  <button class="sales-order-control" onClick="{{ row.moveDown }}" disabled="{{ row.isLast }}" title="Bajar lote" aria-label="Bajar lote"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button>
7606                  <input class="sales-order-select" type="number" min="1" max="{{ row.maxPosition }}" data-dc-attr-value="{{ row.position }}" onChange="{{ row.setPosition }}" aria-label="Posición del certificado {{ row.cert }}">
7607                </div>
7608                <sc-if value="{{ row.videoOpen }}" hint-placeholder-val="false"><div class="sales-order-video-panel"><div><strong>Video del lote</strong><span class="sales-order-video-badge {{ row.videoBadgeClass }}">{{ row.videoLabel }}</span><sc-if value="{{ row.videoIsRaw }}" hint-placeholder-val="false"><small>No hay video final; se muestra material crudo.</small></sc-if><a class="sales-order-video-direct" data-dc-attr-href="{{ row.videoUrl }}" target="_blank" rel="noopener">Abrir video directamente ↗</a></div><span class="sales-order-video-stage"><video id="sales-order-video-{{ row.cert }}" data-dc-attr-src="{{ row.videoUrl }}" data-dc-attr-poster="{{ row.videoPoster }}" onPlay="{{ row.videoDidPlay }}" onPlaying="{{ row.videoDidPlay }}" onPause="{{ row.videoDidPause }}" onEnded="{{ row.videoDidPause }}" onError="{{ row.videoDidError }}" controls playsinline preload="metadata"></video><button type="button" class="sales-order-video-play" onClick="{{ row.playVideo }}" aria-label="Reproducir video del lote {{ row.cert }}"><span aria-hidden="true">▶</span> Reproducir</button></span></div></sc-if>
7609              </article>
7610            </sc-for>
7611          </div>
7612          <sc-if value="{{ rgSalesOrderNoResults }}"><div class="sales-order-empty">No hay lotes que coincidan con la búsqueda y los filtros.</div></sc-if>
7613          <sc-if value="{{ rgSalesOrderEmpty }}"><div class="sales-order-empty">Este remate todavía no tiene lotes para ordenar.</div></sc-if>
7614        </div>
7615      </main>
7616    </sc-if>
7617    <sc-if value="{{ isCabanaSalesOrder }}" hint-placeholder-val="false">
7618      <main class="sales-order-page">
7619        <div class="sales-order-shell">
7620          <button class="sales-order-back" onClick="{{ closeCbSalesOrder }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button>
7621          <header class="sales-order-head">
7622            <div>
7623              <span class="sales-order-kicker">ADMINISTRACIÓN DEL REMATE</span>
7624              <h1>Orden de ventas</h1>
7625              <p>{{ cbSalesOrderAuctionTitle }}</p>
7626            </div>
7627            <div class="sales-order-toolbar">
7628              <button class="sales-order-action" onClick="{{ toggleCbSalesOrderGrouped }}" style="border-color:{{ cbSalesGroupBorder }}; background:{{ cbSalesGroupBg }}; color:{{ cbSalesGroupColor }};">
7629                <img src="assets/icons/lucide/layers-2.svg" alt="">
7630                {{ cbSalesOrderGroupLabel }}
7631              </button>
7632              <button class="sales-order-action primary" onClick="{{ saveCbSalesOrder }}
7632" style="background:{{ cbColor }}; opacity:{{ cbSalesOrderSaveOpacity }};">
7633                <img src="assets/icons/lucide/save.svg" alt="">
7634                {{ cbSalesOrderSaveLabel }}
7635              </button>
7636            </div>
7637          </header>
7638          <div class="sales-order-summary"><strong>{{ cbSalesOrderCount }}</strong> lotes. Usá las flechas o elegí una posición para definir el orden visible en el catálogo.</div>
7639          <sc-if value="{{ cbSalesOrderSaved }}"><div class="sales-order-message is-success">Orden guardado correctamente.</div></sc-if>
7640          <sc-if value="{{ cbSalesOrderHasError }}"><div class="sales-order-message is-error">{{ cbSalesOrderError }}</div></sc-if>
7641          <sc-if value="{{ cbSalesOrderLoading }}"><div class="sales-order-empty"><span class="dc-spinner"></span> Cargando lotes...</div></sc-if>
7642          <sc-if value="{{ cbSalesOrderFlat }}">
7643            <div class="sales-order-list" data-sales-flat>
7644              <sc-for list="{{ cbSalesOrderRows }}" as="row" hint-placeholder-count="8">
7645                <article class="sales-order-row" data-sales-lot>
7646                  <input class="sales-order-cert" type="hidden" value="{{ row.cert }}">
7647                  <span class="sales-order-position">{{ row.position }}</span>
7648                  <div class="sales-order-copy"><strong>{{ row.title }}</strong><small>{{ row.category }} · {{ row.cabana }}</small></div>
7649                  <div class="sales-order-controls">
7650                    <button class="sales-order-control" onClick="{{ row.moveUp }}" disabled="{{ row.isFirst }}" title="Subir lote" aria-label="Subir lote"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button>
7651                    <button class="sales-order-control" onClick="{{ row.moveDown }}" disabled="{{ row.isLast }}" title="Bajar lote" aria-label="Bajar lote"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button>
7652                    <select class="sales-order-select" value="{{ row.position }}" onChange="{{ row.setPosition }}" aria-label="Posición del lote">
7653                      <sc-for list="{{ row.positions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for>
7654                    </select>
7655                  </div>
7656                </article>
7657              </sc-for>
7658            </div>
7659          </sc-if>
7660          <sc-if value="{{ cbSalesOrderGrouped }}">
7661            <div class="sales-order-groups" data-sales-groups>
7662              <sc-for list="{{ cbSalesOrderGroups }}" as="group" hint-placeholder-count="3">
7663                <section class="sales-order-category" data-sales-category>
7664                  <input class="sales-order-category-key" type="hidden" value="{{ group.key }}">
7665                  <header class="sales-order-category-head">
7666                    <div><strong>{{ group.title }}</strong><small>{{ group.count }} lotes</small></div>
7667                    <div class="sales-order-controls">
7668                      <button class="sales-order-control" onClick="{{ group.moveUp }}" disabled="{{ group.isFirst }}" title="Subir categoría" aria-label="Subir categoría"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button>
7669                      <button class="sales-order-control" onClick="{{ group.moveDown }}" disabled="{{ group.isLast }}" title="Bajar categoría" aria-label="Bajar categoría"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button>
7670                      <select class="sales-order-select" value="{{ group.position }}" onChange="{{ group.setPosition }}" aria-label="Posición de la categoría">
7671                        <sc-for list="{{ group.positions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for>
7672                      </select>
7673                    </div>
7674                  </header>
7675                  <div class="sales-order-category-list" data-sales-category-list>
7676                    <sc-for list="{{ group.lots }}" as="row" hint-placeholder-count="4">
7677                      <article class="sales-order-row" data-sales-lot>
7678                        <input class="sales-order-cert" type="hidden" value="{{ row.cert }}">
7679                        <span class="sales-order-position">{{ row.position }}</span>
7680                        <div class="sales-order-copy"><strong>{{ row.title }}</strong><small>{{ row.cabana }}</small></div>
7681                        <div class="sales-order-controls">
7682                          <button class="sales-order-control" onClick="{{ row.moveUp }}" disabled="{{ row.isFirst }}" title="Subir lote" aria-label="Subir lote"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button>
7683                          <button class="sales-order-control" onClick="{{ row.moveDown }}" disabled="{{ row.isLast }}" title="Bajar lote" aria-label="Bajar lote"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button>
7684                          <select class="sales-order-select" value="{{ row.groupPosition }}" onChange="{{ row.setPosition }}" aria-label="Posición del lote en la categoría">
7685                            <sc-for list="{{ row.positions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for>
7686                          </select>
7687                        </div>
7688                      </article>
7689                    </sc-for>
7690                  </div>
7691                </section>
7692              </sc-for>
7693            </div>
7694          </sc-if>
7695          <sc-if value="{{ cbSalesOrderEmpty }}"><div class="sales-order-empty">Este remate todavía no tiene lotes para ordenar.</div></sc-if>
7696        </div>
7697      </main>
7698    </sc-if>
7699
7700    <!-- FICHA DE REPRODUCTOR -->
7701    <sc-if value="{{ isCabanaLotDetail }}" hint-placeholder-val="false">
7702      <main style="width:100%; max-width:1240px; min-width:0; box-sizing:border-box; overflow-x:hidden; margin:0 auto; padding:26px clamp(14px,4vw,36px) 100px; animation:pageIn 360ms both;">
7703        <button onClick="{{ goCabanaBack }}" style="display:inline-flex;align-items:center;gap:6px;border:0; background:transparent; color:#0a5ca8; font:700 13px inherit; padding:0; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button>
7704        <div style="display:flex; justify-content:space-between; align-items:end; gap:12px; flex-wrap:wrap; margin:10px 0 18px;"><div><h1 style="margin:0; font-size:clamp(26px,3.4vw,36px); color:#112033;">{{ clTitle }}</h1><span style="font-size:13px; color:#65758a;">{{ clSubtitle }}</span></div><div class="dc-lot-social-actions"><sc-if value="{{ clCanEdit }}" hint-placeholder-val="false"><button class="dc-lot-social-button" onClick="{{ clEditPricing }}">Base e incremento</button><button class="dc-lot-social-button" onClick="{{ clEdit }}" title="Editar lote" aria-label="Editar lote"><img src="assets/icons/lucide/pencil.svg" alt="">Editar lote</button></sc-if><sc-if value="{{ clCanDelete }}" hint-placeholder-val="false"><button class="dc-lot-social-button" onClick="{{ clDelete }}" disabled="{{ clDeleting }}" title="Eliminar lote" aria-label="Eliminar lote" style="border-color:#f0c5ca; color:#b4232f; opacity:{{ clDeleteOpacity }}; cursor:{{ clDeleteCursor }};"><img src="assets/icons/lucide/trash-2.svg" alt="">{{ clDeleteLabel }}</button></sc-if><sc-if value="{{ clCanDeleteVideo }}" hint-placeholder-val="false"><button class="dc-lot-social-button" onClick="{{ clDeleteVideo }}" disabled="{{ clVideoDeleting }}" title="Eliminar el video seleccionado" aria-label="Eliminar el video seleccionado" style="border-color:#f0c5ca;color:#b4232f;"><img src="assets/icons/lucide/trash-2.svg" alt="">{{ clDeleteVideoLabel }}</button></sc-if><button class="dc-lot-social-button dc-lot-social-button--instagram" onClick="{{ clShareInstagramStory }}" title="Compartir en Instagram Stories" aria-label="Compartir en Instagram Stories"><img src="assets/icons/brands/instagram.svg" alt="">{{ clInstagramStoryLabel }}</button><button class="dc-lot-social-button dc-lot-social-button--whatsapp" onClick="{{ clShareWhatsappStatus }}" title="Compartir en WhatsApp Estado" aria-label="Compartir en WhatsApp Estado"><img src="assets/icons/brands/whatsapp.svg" alt="">{{ clWhatsappStatusLabel }}</button><button class="dc-lot-social-button" onClick="{{ clShare }}" title="Compartir este RP" aria-label="Compartir este RP"><img src="assets/icons/lucide/share-2.svg" alt="">Compartir <strong>{{ clShareCount }}</strong></button><sc-if value="{{ clShowStatus }}" hint-placeholder-val="false"><span style="padding:7px 14px; border-radius:100px; color:#fff; background:{{ clStatusBg }}; font-size:11px; font-weight:900;">{{ clStatus }}</span></sc-if></div></div>
7705        <sc-if value="{{ clHasRpVariants }}"><section style="margin:-4px 0 18px; padding:12px 14px; border:1px solid #dce6f0; border-radius:12px; background:#fff;"><small style="display:block; margin-bottom:8px; color:#728399; font-size:10px; font-weight:900; letter-spacing:.09em;">ANIMALES DEL LOTE</small><div style="display:flex; gap:9px; overflow-x:auto; padding:1px 1px 4px;"><sc-for list="{{ clRpVariants }}" as="variant"><button class="{{ variant.className }}" onClick="{{ variant.select }}" title="{{ variant.title }}" aria-label="{{ variant.title }}"><sc-if value="{{ variant.hasThumb }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ variant.thumb }}" alt="{{ variant.label }}" style="display:block; width:100%; height:100%; object-fit:cover;"></sc-if><sc-if value="{{ variant.noThumb }}" hint-placeholder-val="false"><span style="display:grid; place-items:center; width:100%; height:100%; color:#60758f; font-size:11px; font-weight:900;">{{ variant.shortLabel }}</span></sc-if><span class="dc-rp-variant-label">{{ variant.label }}</span><sc-if value="{{ variant.active }}" hint-placeholder-val="false"><span class="dc-rp-variant-check"><img src="assets/icons/lucide/check.svg" alt=""></span></sc-if></button></sc-for></div></section></sc-if>
7706        <div style="width:100%; min-width:0; display:grid; grid-template-columns:{{ cabanaDetailCols }}
7706; gap:20px; align-items:start;">
7707          <div style="display:grid; gap:12px; min-width:0;">
7708             <div class="dc-cabana-video-frame"><sc-if value="{{ clMediaLoading }}"><div class="dc-media-loader"><span class="dc-spinner"></span></div></sc-if><sc-if value="{{ clHasVideo }}" hint-placeholder-val="false"><video class="dc-cabana-lot-player" data-dc-attr-src="{{ clVideoUrl }}" data-dc-attr-poster="{{ clVideoPoster }}" onCanPlay="{{ clMediaLoaded }}" onLoadedMetadata="{{ clVideoMeta }}" onTimeUpdate="{{ clVideoTimeUpdate }}" onVolumeChange="{{ clVideoVolumeChange }}" onPause="{{ clVideoPause }}" onPlay="{{ clVideoPlay }}" onPlaying="{{ clVideoPlay }}" onEnded="{{ clVideoEnded }}" onError="{{ clMediaError }}" controls autoplay muted loop preload="auto" playsinline style="width:100%; height:100%; object-fit:contain;background:#000;"></video></sc-if><sc-if value="{{ clShowImage }}" hint-placeholder-val="false"><img class="dc-media-image" data-dc-attr-src="{{ clImage }}" onLoad="{{ clMediaLoaded }}" onError="{{ clMediaError }}" alt="{{ clName }}" style="width:100%; height:100%; object-fit:cover;"></sc-if><sc-if value="{{ clPlaybackError }}" hint-placeholder-val="false"><div class="dc-cabana-playback-error" role="alert" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;text-align:center;background:rgba(0,24,45,.92);color:#fff;z-index:2;"><strong>No se pudo reproducir este archivo</strong><span>El material está cargado. Reintentá la reproducción.</span><button type="button" onClick="{{ clRetryMedia }}" style="padding:12px 22px;border:0;border-radius:12px;background:white;color:#003466;font:700 15px inherit;cursor:pointer;">Reintentar</button></div></sc-if><sc-if value="{{ clEmptyMedia }}" hint-placeholder-val="true"><div class="dc-cabana-empty-media"><span>Sin material audiovisual cargado</span><sc-if value="{{ clUploadMediaBusy }}" hint-placeholder-val="false"><div class="dc-cabana-upload-progress" role="progressbar" aria-label="Progreso de carga del video" aria-valuemin="0" aria-valuemax="100" data-dc-attr-aria-valuenow="{{ clUploadProgress }}"><div class="dc-cabana-upload-ring" style="--upload-progress:{{ clUploadProgress }}%;"><strong>{{ clUploadProgress }}%</strong></div><div class="dc-cabana-upload-copy"><strong>{{ clUploadStage }}</strong><div class="dc-cabana-upload-track"><span style="width:{{ clUploadProgress }}%;"></span></div><small>No cierres esta pantalla hasta finalizar.</small></div></div></sc-if><sc-if value="{{ clCanSelectUpload }}" hint-placeholder-val="false"><label class="dc-cabana-empty-upload"><img src="assets/icons/lucide/upload.svg" alt="">Subir video<input type="file" accept="video/*" multiple onChange="{{ clUploadMedia }}" style="display:none;"></label><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guía en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">↗</span></a></sc-if></div></sc-if></div>
7709             <sc-if value="{{ clHasVideo }}" hint-placeholder-val="false"><div class="dc-video-controls" aria-label="Controles del video"><button class="dc-video-control-button" onClick="{{ clTogglePlay }}" title="{{ clPlayLabel }}" aria-label="{{ clPlayLabel }}"><img data-dc-attr-src="{{ clPlayIcon }}" alt=""></button><input class="dc-video-slider" type="range" min="0" max="100" step="0.1" value="{{ clVideoProgress }}" onInput="{{ clSeekVideo }}" onChange="{{ clSeekVideo }}" aria-label="Avance del video"><span class="dc-video-time">{{ clVideoTime }}</span><button class="dc-video-control-button" onClick="{{ clToggleMute }}" title="{{ clMuteLabel }}" aria-label="{{ clMuteLabel }}"><img data-dc-attr-src="{{ clVolumeIcon }}" alt=""></button><input class="dc-video-slider dc-video-volume" type="range" min="0" max="1" step="0.05" value="{{ clVideoVolume }}" onInput="{{ clSetVolume }}" onChange="{{ clSetVolume }}" aria-label="Volumen"><button class="dc-video-control-button" onClick="{{ clFullscreen }}" title="Pantalla completa" aria-label="Pantalla completa"><img src="assets/icons/lucide/maximize-2.svg" alt=""></button></div></sc-if>
7710             <sc-if value="{{ clHasThumbs }}
7710" hint-placeholder-val="false"><div style="display:flex; gap:10px; overflow-x:auto; padding:2px 2px 4px;"><sc-for list="{{ clThumbs }}" as="thumb" hint-placeholder-count="3"><button onClick="{{ thumb.select }}" title="{{ thumb.label }}" style="position:relative; width:126px; aspect-ratio:16/9; flex:0 0 auto; border:2px solid {{ thumb.border }}; border-radius:11px; overflow:hidden; background:#edf2f7; padding:0; cursor:pointer; box-shadow:0 8px 20px rgba(0,30,60,.08);"><sc-if value="{{ thumb.hasThumb }}" hint-placeholder-val="false"><img class="dc-media-image" data-dc-attr-src="{{ thumb.thumb }}" onLoad="{{ thumb.mediaReady }}" onError="{{ thumb.mediaError }}" alt="{{ thumb.label }}" style="width:100%; height:100%; object-fit:cover;"></sc-if><span style="position:absolute; left:7px; bottom:7px; padding:3px 7px; border-radius:999px; background:rgba(0,29,61,.72); color:#fff; font-size:10px; font-weight:900; letter-spacing:.02em;">{{ thumb.icon }}</span></button></sc-for></div></sc-if>
7711             <sc-if value="{{ isMobile }}" hint-placeholder-val="{{ false }}"><aside style="display:grid; gap:12px; min-width:0; width:100%;"><sc-if value="{{ clShowBaseCard }}" hint-placeholder-val="false"><section style="border-radius:18px; padding:21px; color:#fff; background:{{ clPriceBackground }}; box-shadow:0 18px 48px rgba(0,30,60,.2);"><small style="font-weight:900; letter-spacing:.1em; opacity:.75;">{{ clPriceLabel }}</small><strong style="display:block; margin-top:5px; font-size:34px;">{{ clPriceAmount }}</strong><sc-if value="{{ clOwnWinning }}"><span style="display:inline-block;margin-top:10px;padding:5px 9px;border-radius:7px;background:rgba(255,255,255,.18);font-weight:800;font-size:13px;">{{ clOwnWinningLabel }}</span></sc-if><sc-if value="{{ clInstallmentsEnabled }}"><small style="display:block;margin-top:8px;">{{ clInstallmentPriceTotal }}</small></sc-if></section></sc-if><sc-if value="{{ clPrebidEnabled }}" hint-placeholder-val="false"><section class="dc-prebid-side-card {{ clPrebidCardClass }}"><div class="dc-prebid-side-head"><span class="dc-prebid-side-icon"><img src="assets/icons/lucide/gavel.svg" alt=""></span><span class="dc-prebid-side-copy"><small>PREOFERTAS</small></span></div><div class="dc-prebid-side-actions"><sc-if value="{{ clPrebidUpcoming }}" hint-placeholder-val="false"><div class="dc-prebid-start"><strong>Las preofertas comienzan</strong><span>{{ clPrebidStartLabel }}</span><prebid-start-countdown start="{{ clPrebidStart }}"></prebid-start-countdown></div></sc-if><sc-if value="{{ clPrebidClosed }}" hint-placeholder-val="false"><p class="dc-prebid-closed" role="status">Preofertas cerradas</p></sc-if><sc-if value="{{ clPrebidOpen }}" hint-placeholder-val="false"><button class="dc-prebid-side-button" onClick="{{ clOpenPrebid }}"><img src="assets/icons/lucide/badge-dollar-sign.svg" alt="">Preofertar</button></sc-if><button type="button" class="dc-prebid-history-button" onClick="{{ clViewPrebidHistory }}">Ver historial de preofertas <span aria-hidden="true">↓</span></button></div></section></sc-if><sc-if value="{{ clHasCabanaLogo }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:16px;"><small style="display:block; color:#8593a6; margin-bottom:10px;">CABAÑA</small><img data-dc-attr-src="{{ clCabanaLogo }}" alt="{{ clCabanaName }}" style="display:block; width:100%; max-height:96px; object-fit:contain; object-position:left center;"></section></sc-if><sc-if value="{{ clShowCabanaNameCard }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">CABAÑA</small><strong>{{ clCabanaName }}</strong></section></sc-if><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">REMATE</small><strong>{{ clCabana }}</strong></section><section style="display:flex; align-items:center; gap:12px; min-width:0; color:#fff; background:{{ clConsignatariaColor }}; border-radius:15px; padding:14px 15px; box-shadow:0 10px 26px {{ clConsignatariaShadow }};"><span class="dc-consignataria-logo-box" style="width:42px; height:42px; flex:0 0 42px;"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt=""></sc-if>
7711<sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong style="font-size:18px;">{{ clConsignatariaInitial }}</strong></sc-if></span><span style="min-width:0;"><small style="display:block; color:rgba(255,255,255,.78); font-weight:800; letter-spacing:.07em;">CONSIGNATARIA</small><strong style="display:block; color:#fff; line-height:1.2;">{{ clConsignataria }}</strong></span></section><section class="dc-lot-contact-card"><div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar" style="background:{{ clConsignatariaColor }};"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt="{{ clConsignataria }}"></sc-if><sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ clConsignatariaInitial }}</strong></sc-if></span><span class="dc-lot-contact-copy"><small>CONTACTO DEL LOTE</small><strong>{{ clContactName }}</strong><span>{{ clContactPhone }}</span></span></div><div class="dc-lot-contact-actions"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ clContactWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">Consultar por WhatsApp</a><a class="dc-contact-call" data-dc-attr-href="{{ clContactPhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></div></section></aside></sc-if>
7712             <sc-for list="{{ clPrimaryGroups }}" as="group" hint-placeholder-count="1"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em; margin-bottom:11px;">{{ group.label }}</small><div class="rg-detail-list"><sc-for list="{{ group.cards }}" as="data" hint-placeholder-count="4"><div class="dc-rosgan-field-card rg-detail-row" style="grid-template-columns:minmax(0,1fr);"><span class="dc-rosgan-field-copy"><small>{{ data.label }}</small><strong>{{ data.value }}</strong></span></div></sc-for></div></section></sc-for>
7713            <sc-for list="{{ clTables }}" as="table" hint-placeholder-count="1">
7714              <section style="min-width:0; background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 10px 28px rgba(0,30,60,.05);">
7715                <small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em; margin-bottom:11px;">{{ table.title }}</small>
7716                <sc-if value="{{ isDesktop }}" hint-placeholder-val="{{ true }}">
7717                  <div style="max-width:100%; overflow-x:auto; border:1px solid #e5edf5; border-radius:14px;">
7718                    <table style="width:100%; min-width:720px; border-collapse:separate; border-spacing:0; font-size:13px;">
7719                      <thead><tr style="background:#f5f8fb;"><th style="position:sticky; left:0; z-index:1; background:#f5f8fb; color:#65758a; text-align:left; padding:12px 14px; font-size:11px; letter-spacing:.08em;">FORMULARIO</th><sc-for list="{{ table.columns }}" as="column" hint-placeholder-count="6"><th style="color:#65758a; text-align:left; padding:12px 14px; font-size:11px; letter-spacing:.08em; white-space:nowrap;">{{ column.label }}</th></sc-for></tr></thead>
7720                      <tbody><sc-for list="{{ table.rows }}" as="row" hint-placeholder-count="2"><tr><th style="position:sticky; left:0; z-index:1; background:#fff; color:#0a5ca8; text-align:left; padding:13px 14px; border-top:1px solid #edf2f7; white-space:nowrap; font-size:13px;">{{ row.label }}</th><sc-for list="{{ row.cells }}" as="cell" hint-placeholder-count="6"><td style="padding:13px 14px; border-top:1px solid #edf2f7; color:#112033; font-weight:800; white-space:nowrap;">{{ cell.value }}</td></sc-for></tr></sc-for></tbody>
7721                    </table>
7722                  </div>
7723                </sc-if>
7724                <sc-if value="{{ isMobile }}" hint-placeholder-val="{{ false }}">
7725                  <div style="width:100%; max-width:100%; overflow:hidden; border:1px solid #e5edf5; border-radius:12px;">
7726                    <table style="width:100%; max-width:100%; table-layout:fixed; border-collapse:collapse;">
7727                      <thead><tr style="background:#f5f8fb;"><th style="width:48px; padding:8px 3px; color:#65758a; text-align:left;
7727 font-size:7px; line-height:1; letter-spacing:0;">FORM.</th><sc-for list="{{ table.columns }}" as="column" hint-placeholder-count="6"><th title="{{ column.label }}" style="padding:8px 1px; color:#65758a; text-align:center; font-size:7px; line-height:1; letter-spacing:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ column.label }}</th></sc-for></tr></thead>
7728                      <tbody><sc-for list="{{ table.rows }}" as="row" hint-placeholder-count="2"><tr><th title="{{ row.label }}" style="padding:10px 3px; border-top:1px solid #edf2f7; color:#0a5ca8; text-align:left; font-size:9px; line-height:1.05; letter-spacing:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ row.mobileLabel }}</th><sc-for list="{{ row.cells }}" as="cell" hint-placeholder-count="6"><td title="{{ cell.label }}: {{ cell.value }}" style="padding:10px 1px; border-top:1px solid #edf2f7; color:#112033; text-align:center; font-size:9px; line-height:1.05; font-weight:800; letter-spacing:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ cell.value }}</td></sc-for></tr></sc-for></tbody>
7729                    </table>
7730                  </div>
7731                </sc-if>
7732              </section>
7733            </sc-for>
7734            <sc-for list="{{ clSecondaryGroups }}" as="group" hint-placeholder-count="2"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em; margin-bottom:11px;">{{ group.label }}</small><div class="rg-detail-list"><sc-for list="{{ group.cards }}" as="data" hint-placeholder-count="4"><div class="dc-rosgan-field-card rg-detail-row" style="grid-template-columns:minmax(0,1fr);"><span class="dc-rosgan-field-copy"><small>{{ data.label }}</small><strong>{{ data.value }}</strong></span></div></sc-for></div></section></sc-for>
7735            <sc-if value="{{ clHasObservations }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em;">OBSERVACIONES</small><p style="margin:9px 0 0; color:#3d4c5f; font-size:14px; line-height:1.55;">{{ clObservations }}</p></section></sc-if>
7736          </div>
7737          <sc-if value="{{ isDesktop }}" hint-placeholder-val="{{ true }}"><aside style="position:{{ cabanaSticky }}; top:76px; display:grid; gap:12px; min-width:0; width:100%;"><sc-if value="{{ clShowBaseCard }}" hint-placeholder-val="false"><section style="border-radius:18px; padding:21px; color:#fff; background:{{ clPriceBackground }}; box-shadow:0 18px 48px rgba(0,30,60,.2);"><small style="font-weight:900; letter-spacing:.1em; opacity:.75;">{{ clPriceLabel }}</small><strong style="display:block; margin-top:5px; font-size:34px;">{{ clPriceAmount }}</strong><sc-if value="{{ clOwnWinning }}"><span style="display:inline-block;margin-top:10px;padding:5px 9px;border-radius:7px;background:rgba(255,255,255,.18);font-weight:800;font-size:13px;">{{ clOwnWinningLabel }}</span></sc-if><sc-if value="{{ clInstallmentsEnabled }}"><small style="display:block;margin-top:8px;">{{ clInstallmentPriceTotal }}</small></sc-if></section></sc-if><sc-if value="{{ clPrebidEnabled }}" hint-placeholder-val="false"><section class="dc-prebid-side-card {{ clPrebidCardClass }}"><div class="dc-prebid-side-head"><span class="dc-prebid-side-icon"><img src="assets/icons/lucide/gavel.svg" alt=""></span><span class="dc-prebid-side-copy"><small>PREOFERTAS</small></span></div><div class="dc-prebid-side-actions"><sc-if value="{{ clPrebidUpcoming }}" hint-placeholder-val="false"><div class="dc-prebid-start"><strong>Las preofertas comienzan</strong><span>{{ clPrebidStartLabel }}</span><prebid-start-countdown start="{{ clPrebidStart }}"></prebid-start-countdown></div></sc-if><sc-if value="{{ clPrebidClosed }}" hint-placeholder-val="false"><p class="dc-prebid-closed" role="status">Preofertas cerradas</p></sc-if><sc-if value="{{ clPrebidOpen }}" hint-placeholder-val="false"><button class="dc-prebid-side-button" onClick="{{ clOpenPrebid }}"><img src="assets/icons/lucide/badge-dollar-sign.svg" alt="">Preofertar</button></sc-if><button type="button" class="dc-prebid-history-button" onClick="{{ clViewPrebidHistory }}">Ver historial de preofertas <span aria-hidden="true">↓</span></button></div></section></sc-if><sc-if value="{{ clHasCabanaLogo }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:16px;"><small style="display:block; color:#8593a6; margin-bottom:10px;">CABAÑA</small><img data-dc-attr-src="{{ clCabanaLogo }}" alt="{{ clCabanaName }}" style="display:block; width:100%; max-height:96px; object-fit:contain; object-position:left center;"></section></sc-if><sc-if value="{{ clShowCabanaNameCard }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">CABAÑA</small><strong>{{ clCabanaName }}</strong></section></sc-if><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">REMATE</small><strong>{{ clCabana }}</strong></section><section style="display:flex; align-items:center; gap:12px; min-width:0; color:#fff; background:{{ clConsignatariaColor }}; border-radius:15px; padding:14px 15px; box-shadow:0 10px 26px {{ clConsignatariaShadow }};"><span class="dc-consignataria-logo-box" style="width:42px; height:42px; flex:0 0 42px;"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt=""></sc-if>
7737<sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong style="font-size:18px;">{{ clConsignatariaInitial }}</strong></sc-if></span><span style="min-width:0;"><small style="display:block; color:rgba(255,255,255,.78); font-weight:800; letter-spacing:.07em;">CONSIGNATARIA</small><strong style="display:block; color:#fff; line-height:1.2;">{{ clConsignataria }}</strong></span></section><section class="dc-lot-contact-card"><div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar" style="background:{{ clConsignatariaColor }};"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt="{{ clConsignataria }}"></sc-if><sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ clConsignatariaInitial }}</strong></sc-if></span><span class="dc-lot-contact-copy"><small>CONTACTO DEL LOTE</small><strong>{{ clContactName }}</strong><span>{{ clContactPhone }}</span></span></div><div class="dc-lot-contact-actions"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ clContactWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">Consultar por WhatsApp</a><a class="dc-contact-call" data-dc-attr-href="{{ clContactPhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></div></section></aside></sc-if>
7738        </div>
7739        <sc-if value="{{ clPrebidEnabled }}" hint-placeholder-val="false">
7740          <section id="cabana-prebid-history" tabindex="-1" aria-label="Mis preofertas" style="scroll-margin-top:80px; margin-top:20px; overflow:hidden; border:1px solid #d9e5ef; border-radius:20px; background:#fff; box-shadow:0 16px 40px rgba(0,46,95,.08);">
7741            <div style="padding:18px clamp(16px,3vw,25px) 23px;">
7742              <small style="display:block; margin-bottom:12px; color:#60758a; font-size:10px; font-weight:900; letter-spacing:.12em;">MIS PREOFERTAS</small>
7743              <sc-if value="{{ clPrebidLoading }}" hint-placeholder-val="false"><div style="display:flex; align-items:center; justify-content:center; gap:9px; min-height:90px; color:#60758a; font-size:13px;"><span class="dc-spinner"></span>Cargando preofertas...</div></sc-if>
7744              <sc-if value="{{ clPrebidHasError }}" hint-placeholder-val="false"><div style="padding:13px 15px; border-radius:12px; background:#fff1f2; color:#a8212d; font-size:12px; font-weight:750;">{{ clPrebidError }}</div></sc-if>
7745              <sc-if value="{{ clPrebidHasHistory }}" hint-placeholder-val="false"><div style="display:grid; gap:8px;"><sc-for list="{{ clPrebidHistory }}" as="offer"><article style="display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:8px 12px; min-width:0; padding:12px 14px; border:1px solid #e5edf4; border-radius:13px; background:#fbfdff;"><div style="min-width:0;"><strong style="display:block; color:#102f50; font-size:16px;">{{ offer.amount }}</strong><small style="color:#7a8b9f; font-size:10px;">{{ offer.date }}</small></div><span style="justify-self:end; padding:6px 10px; border-radius:999px; background:{{ offer.statusBg }}; color:{{ offer.statusColor }}; font-size:10px; font-weight:900; white-space:nowrap;">{{ offer.status }}</span><sc-if value="{{ offer.hasCommission }}" hint-placeholder-val="false"><small style="grid-column:1/-1; display:block; color:#7a8b9f; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ offer.commission }}</small></sc-if><sc-if value="{{ offer.canDelete }}"><button disabled="{{ offer.deleteBusy }}" onClick="{{ offer.deleteOffer }}" style="padding:8px 12px; border:1px solid #efb7bf; border-radius:9px; color:#b51f35; background:#fff; cursor:pointer;">Eliminar preoferta</button></sc-if></article></sc-for></div></sc-if>
7746              <sc-if value="{{ clPrebidEmpty }}" hint-placeholder-val="false"><div style="display:flex; align-items:center; gap:12px; padding:14px; border:1px dashed #cad8e5; border-radius:14px; color:#60758a;"><span style="display:grid; place-items:center; width:38px; height:38px; flex:0 0 38px; border-radius:12px; background:#edf5fb;"><img src="assets/icons/lucide/circle-plus.svg" alt="" style="width:18px; height:18px;"></span><span><strong style="display:block; color:#27435f; font-size:13px;">Todavía no hiciste preofertas</strong><small>Acá vas a ver tus preofertas de este lote.</small></span></div></sc-if>
7747            </div>
7748          </section>
7749        </sc-if>
7750      </main>
7751      <sc-if value="{{ clPrebidSuccessOpen }}" hint-placeholder-val="false">
7752        <div class="prebid-success-layer" role="status" aria-live="assertive" aria-label="Preoferta confirmada">
7753          <div class="prebid-success-flood" aria-hidden="true"></div>
7754          <div class="prebid-success-content">
7755            <span class="prebid-success-check"><img src="assets/icons/lucide/check.svg" alt=""></span>
7756            <div class="prebid-success-copy">
7757              <small>{{ clPrebidSuccessEyebrow }}</small>
7758              <h2>{{ clPrebidSuccessTitle }}</h2>
7759              <strong class="prebid-success-amount">{{ clPrebidSuccessAmount }}</strong>
7760              <p>{{ clPrebidSuccessMessage }}</p>
7761            </div>
7762          </div>
7763        </div>
7764      </sc-if>
7765      <sc-if value="{{ clPrebidModalOpen }}" hint-placeholder-val="false">
7766        <div role="dialog" aria-modal="true" aria-label="Realizar preoferta" style="position:fixed; inset:0; z-index:100000; display:grid; place-items:center; padding:18px; background:rgba(0,28,59,.72); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); overflow-y:auto;">
7767          <section style="width:min(620px,100%); max-height:calc(100vh - 36px); overflow-y:auto; box-sizing:border-box; border-radius:22px; background:#fff; box-shadow:0 30px 90px rgba(0,20,45,.34);">
7768            <header style="position:sticky; top:0; z-index:2; display:flex; align-items:flex-start; justify-content:space-between; gap:18px; padding:20px 22px 16px; border-bottom:1px solid #e5edf4; background:#fff;"><div><small style="display:block; color:#0a5ca8; font-size:10px; font-weight:900; letter-spacing:.12em;">PREOFERTA DE CABAÑA</small><h2 style="margin:4px 0 0; color:#102f50; font-size:24px; line-height:1.1;">Hacé tu preoferta</h2><p style="margin:6px 0 0; color:#718196; font-size:12px;">Actual: <strong style="color:#102f50;">{{ clPrebidCurrentAmount }}</strong> · incremento {{ clPrebidIncrement }}</p></div><button onClick="{{ clClosePrebid }}" title="Cerrar" aria-label="Cerrar" style="display:grid; place-items:center; width:38px; height:38px; flex:0 0 38px; border:1px solid #d9e4ee; border-radius:12px; background:#f7fafc; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="" style="width:18px; height:18px;"></button></header>
7769            <div style="display:grid; gap:17px; padding:20px 22px 23px;">
7770              <sc-if value="{{ clPrebidNeedsCuit }}"><label style="display:grid; gap:7px; color:#4e6278; font-size:11px; font-weight:900; letter-spacing:.04em;">CUIT / CUIL<input inputmode="numeric" autocomplete="off" maxlength="13" value="" data-dc-attr-value="{{ clPrebidCuit }}" onInput="{{ clSetPrebidCuit }}" placeholder="20421804460" style="height:48px; box-sizing:border-box; border:1px solid #cbd9e6; border-radius:12px; padding:0 13px; color:#102f50; font:750 15px inherit; outline:none;"></label></sc-if>
7771              <sc-if value="{{ clCuitCheckVisible }}"><div role="status" aria-live="polite" style="font-size:12px;color:{{ clCuitCheckColor }};line-height:1.5;">{{ clCuitCheckText }} <sc-if value="{{ clCuitCheckError }}"><button type="button" onClick="{{ clRetryCuit }}" style="margin-left:8px;border:1px solid #cbd9e6;border-radius:8px;padding:6px 10px;background:#fff;color:#0a5ca8;cursor:pointer;">Reintentar</button></sc-if></div></sc-if>
7772              <div style="display:grid; gap:7px;"><span style="color:#4e6278; font-size:11px; font-weight:900; letter-spacing:.04em;">{{ clPrebidAmountLabel }}</span><div style="display:grid; grid-template-columns:48px minmax(0,1fr) 48px; gap:8px;"><button onClick="{{ clPrebidMinus }}" aria-label="Restar un incremento" style="display:grid; place-items:center; border:1px solid #cbd9e6; border-radius:12px; background:#f4f8fb; cursor:pointer;"><img src="assets/icons/lucide/minus.svg" alt="" style="width:19px; height:19px;"></button><span style="position:relative; display:block; min-width:0;"><span aria-hidden="true" style="position:absolute; z-index:3; left:18px; top:50%; transform:translateY(-50%); color:#002e5f; font-family:inherit; font-size:20px; font-weight:900; font-variant-numeric:tabular-nums; line-height:1; pointer-events:none;">$</span><input aria-label="Monto de preoferta" type="text" inputmode="numeric" value="" data-dc-attr-value="{{ clPrebidAmount }}" onInput="{{ clSetPrebidAmount }}" style="width:100%; height:50px; min-width:0; box-sizing:border-box; border:2px solid #0a5ca8; border-radius:12px; padding:0 36px; color:#002e5f; text-align:center; font-family:inherit; font-size:20px; font-weight:900; font-variant-numeric:tabular-nums; line-height:1; outline:none;"><span aria-hidden="true" data-prebid-roll="true" style="position:absolute; z-index:2; inset:2px; display:grid; place-items:center; overflow:hidden; border-radius:10px; background:#fff; color:#002e5f; font-family:inherit; font-size:20px; font-weight:900; font-variant-numeric:tabular-nums; line-height:1; pointer-events:none; opacity:{{ clPrebidRollOpacity }}; transition:{{ clPrebidRollTransition }};">{{ clPrebidAmountRoll }}</span></span><button onClick="{{ clPrebidPlus }}" aria-label="Sumar un incremento" style="display:grid; place-items:center; border:1px solid #cbd9e6; border-radius:12px; background:#f4f8fb; cursor:pointer;"><img src="assets/icons/lucide/plus.svg" alt="" style="width:19px; height:19px;"></button></div><small style="color:#718196;">Monto sugerido: <strong>{{ clPrebidSuggestedAmount }}</strong></small><sc-if value="{{ clInstallmentsEnabled }}"><strong style="display:block;color:#0a5ca8;font-size:13px;">{{ clInstallmentOfferTotal }}</strong></sc-if></div>
7773              <div style="display:grid; gap:9px; padding:14px; border:1px solid #dde7f0; border-radius:14px; background:#f8fbfd;"><div style="display:flex; align-items:center; justify-content:space-between; gap:14px;"><span><strong style="display:block; color:#27435f; font-size:13px;">En comisión</strong><small style="color:#7a8b9f;">Activá esta opción si ofertás para otra persona.</small></span><button onClick="{{ clTogglePrebidCommission }}" role="switch" data-dc-attr-aria-checked="{{ clPrebidCommission }}" aria-label="Activar o desactivar oferta en comisión" style="position:relative; width:48px; height:27px; flex:0 0 48px; border:0; border-radius:999px; background:{{ clPrebidCommissionBg }}; cursor:pointer;"><span style="position:absolute; top:4px; left:{{ clPrebidCommissionLeft }}; width:19px; height:19px; border-radius:50%; background:#fff; box-shadow:0 2px 7px rgba(0,0,0,.2);"></span></button></div><sc-if value="{{ clPrebidCommission }}" hint-placeholder-val="false"><input value="" data-dc-attr-value="{{ clPrebidCommissionFor }}" onInput="{{ clSetPrebidCommissionFor }}
7773" placeholder="Nombre del comprador" style="height:44px; box-sizing:border-box; border:1px solid #cbd9e6; border-radius:11px; padding:0 12px; color:#102f50; font:700 13px inherit; outline:none;"></sc-if></div>
7774              <div style="display:grid; gap:9px;"><span><strong style="display:block; color:#27435f; font-size:13px;">Animales incluidos</strong><small style="color:#7a8b9f;">{{ clPrebidCertMode }}</small></span><sc-if value="{{ clPrebidHasCerts }}" hint-placeholder-val="false"><div style="display:flex; flex-wrap:wrap; gap:8px;"><sc-for list="{{ clPrebidCertOptions }}" as="cert"><button type="button" role="{{ cert.role }}" aria-checked="{{ cert.selected }}" onClick="{{ cert.toggle }}" style="display:inline-flex; align-items:center; gap:7px; min-height:38px; border:1px solid {{ cert.border }}; border-radius:999px; padding:0 12px; background:{{ cert.bg }}; color:{{ cert.color }}; font:850 12px inherit; cursor:{{ cert.cursor }};"><img data-dc-attr-src="{{ cert.icon }}" alt="" style="width:15px; height:15px;">{{ cert.label }}</button></sc-for></div></sc-if></div>
7775              <sc-if value="{{ clPrebidHasMessage }}" hint-placeholder-val="false"><div style="padding:12px 13px; border-radius:12px; background:{{ clPrebidMessageBg }}; color:{{ clPrebidMessageColor }}; font-size:12px; font-weight:800;">{{ clPrebidMessage }}</div></sc-if>
7776              <div style="display:flex; justify-content:flex-end; gap:9px; flex-wrap:wrap; padding-top:2px;"><button onClick="{{ clClosePrebid }}" style="min-height:46px; border:1px solid #cbd9e6; border-radius:12px; padding:0 16px; background:#fff; color:#40566d; font:850 13px inherit; cursor:pointer;">Cancelar</button><button onClick="{{ clSubmitPrebid }}" disabled="{{ clPrebidSubmitting }}" style="min-height:46px; display:inline-flex; align-items:center; justify-content:center; gap:8px; border:0; border-radius:12px; padding:0 19px; background:#0a5ca8; color:#fff; font:900 13px inherit; cursor:pointer; opacity:{{ clPrebidSubmitOpacity }}; box-shadow:0 10px 24px rgba(10,92,168,.24);"><img src="assets/icons/lucide/gavel.svg" alt="" style="width:17px; height:17px; filter:brightness(0) invert(1);">{{ clPrebidSubmitLabel }}</button></div>
7777            </div>
7778          </section>
7779        </div>
7780      </sc-if>
7781    </sc-if>
7782
7783    <sc-if value="{{ isCabanaStats }}" hint-placeholder-val="{{ false }}">
7784      <style>
7785        .cb-stats-page{min-height:100vh;background:#f3f7fb;color:#112033;padding:28px 20px 110px}
7786        .cb-stats-wrap{width:min(1180px,100%);margin:0 auto}
7787        .cb-stats-back{display:inline-flex;align-items:center;gap:8px;margin:0 0 18px;padding:8px 0;border:0;background:transparent;color:#31526f;font:700 13px inherit;cursor:pointer}
7788        .cb-stats-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
7789        .cb-stats-toolbar .cb-stats-back{margin:0}
7790        .cb-stats-export-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap;justify-content:flex-end}
7791        .cb-stats-export-button{min-height:39px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid #cad9e7;border-radius:9px;padding:0 13px;background:#fff;color:#173956;font:850 12px inherit;cursor:pointer;box-shadow:0 5px 14px rgba(0,38,77,.06);transition:transform .18s,border-color .18s,box-shadow .18s}
7792        .cb-stats-export-button:hover{transform:translateY(-1px);border-color:var(--cbs-accent);box-shadow:0 8px 20px rgba(0,38,77,.1)}
7793        .cb-stats-export-button.is-story{border-color:var(--cbs-dark);background:var(--cbs-dark);color:#fff}
7794        .cb-stats-export-button:disabled{cursor:wait;opacity:.58;transform:none;box-shadow:none}
7795        .cb-stats-export-button img{width:16px;height:16px}.cb-stats-export-button.is-story img{filter:brightness(0) invert(1)}
7796        .cb-stats-hero{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,320px);align-items:center;gap:30px;padding:34px 38px;border-radius:8px;background:linear-gradient(120deg,var(--cbs-dark),var(--cbs-accent));color:#fff;box-shadow:0 18px 44px rgba(0,38,77,.18)}
7797        .cb-stats-hero:after{content:"";position:absolute;inset:auto -5% -55% 35%;height:170%;border:1px solid rgba(255,255,255,.18);border-radius:50%;pointer-events:none}
7798        .cb-stats-eyebrow,.cb-stats-section-label{display:block;font-size:11px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}
7799        .cb-stats-eyebrow{color:rgba(255,255,255,.72)}
7800        .cb-stats-hero h1{margin:7px 0 6px;font-size:clamp(30px,4vw,48px);line-height:1.02;letter-spacing:0}
7801        .cb-stats-hero p{max-width:680px;margin:0;color:rgba(255,255,255,.8);font-size:14px;line-height:1.55}
7802        .cb-stats-meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
7803        .cb-stats-meta span{display:inline-flex;align-items:center;gap:7px;min-height:30px;padding:6px 10px;border:1px solid rgba(255,255,255,.22);border-radius:100px;background:rgba(255,255,255,.09);font-size:11px;font-weight:800}
7804        .cb-stats-logo{position:relative;z-index:1;display:grid;place-items:center;min-height:126px;padding:20px;border-radius:8px;background:rgba(255,255,255,.94)}
7805        .cb-stats-logo img{display:block;width:100%;max-height:92px;object-fit:contain}
7806        .cb-stats-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:18px}
7807        .cb-stats-kpi{min-height:124px;padding:18px;border:1px solid #dfe8f1;border-radius:8px;background:#fff}
7808        .cb-stats-kpi small{display:block;min-height:28px;color:#71839a;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
7809        .cb-stats-kpi strong{display:block;margin-top:8px;color:#10253d;font-size:30px;line-height:1;letter-spacing:0}
7810        .cb-stats-kpi span{display:block;margin-top:9px;color:#7c8b9e;font-size:11px;line-height:1.35}
7811        .cb-stats-grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(280px,.8fr);gap:16px;margin-top:16px}
7812        .cb-stats-panel{min-width:0;padding:22px;border:1px solid #dfe8f1;border-radius:8px;background:#fff}
7813        .cb-stats-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:22px}
7814        .cb-stats-section-label{color:var(--cbs-accent)}
7815        .cb-stats-panel h2{margin:4px 0 0;font-size:20px;line-height:1.2}
7816        .cb-stats-panel-head p{margin:4px 0 0;color:#7c8b9e;font-size:12px}
7817        .cb-stats-bar-list{display:grid;gap:17px}
7818        .cb-stats-bar-copy{display:flex;justify-content:space-between;gap:12px;margin-bottom:7px;font-size:12px}
7819        .cb-stats-bar-copy strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
7820        .cb-stats-bar-copy span{flex:0 0 auto;color:#61748a;font-weight:800}
7821        .cb-stats-bar-track{height:7px;overflow:hidden;border-radius:100px;background:#e8eef5}
7822        .cb-stats-bar-fill{height:100%;border-radius:100px;background:linear-gradient(90deg,var(--cbs-dark),var(--cbs-accent))}
7823        .cb-stats-ranking{display:grid;gap:8px}
7824        .cb-stats-rank{display:grid;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid #edf2f7}
7825        .cb-stats-rank:last-child{border-bottom:0}
7826        .cb-stats-rank i{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;background:#edf4fa;color:var(--cbs-accent);font-style:normal;font-size:11px;font-weight:900}
7827        .cb-stats-rank strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}
7828        .cb-stats-rank span{color:#667a91;font-size:11px;font-weight:800}
7829        .cb-stats-quality{display:grid;grid-template-columns:1.1fr 1fr 1fr;gap:0;margin-top:16px;border:1px solid #dfe8f1;border-radius:8px;background:#fff}
7830        .cb-stats-quality-intro,.cb-stats-quality-item{padding:20px}
7831        .cb-stats-quality-item{border-left:1px solid #e5ecf3}
7832        .cb-stats-quality h2{margin:5px 0 0;font-size:19px}
7833        .cb-stats-quality p{margin:6px 0 0;color:#74869a;font-size:12px;line-height:1.45}
7834        .cb-stats-quality-item strong{display:block;font-size:24px}
7835        .cb-stats-quality-item span{display:block;margin-top:7px;color:#74869a;font-size:11px;line-height:1.4}
7836        .cb-stats-empty{padding:24px 0;color:#7c8b9e;font-size:13px;text-align:center}
7837        .cb-stats-overview-list{display:grid;gap:10px;margin-top:16px}
7838        .cb-stats-overview-row{display:grid;grid-template-columns:minmax(220px,1.5fr) repeat(4,minmax(90px,.55fr)) auto;align-items:center;gap:12px;width:100%;padding:16px 18px;border:1px solid #dfe8f1;border-radius:10px;background:#fff;color:#112033;text-align:left;cursor:pointer}
7839        .cb-stats-overview-row:hover{border-color:var(--cbs-accent);box-shadow:0 10px 26px rgba(0,38,77,.08)}
7840        .cb-stats-overview-row strong,.cb-stats-overview-row span{display:block}
7841        .cb-stats-overview-row small{display:block;color:#71839a;font-size:10px;font-weight:900;letter-spacing:.06em;text-transform:uppercase}
7842        .cb-stats-overview-status{padding:6px 10px;border-radius:999px;background:#e8f7f1;color:#087653;font-size:10px;font-weight:900}.cb-stats-overview-status.is-error{background:#fff0f0;color:#b4232f}
7843        .cb-stats-activity{margin-top:16px}
7844        .cb-stats-rp-scroll{overflow-x:auto;margin-top:14px;border:1px solid #dfe8f1;border-radius:10px;background:#fff}
7845        .cb-stats-rp-table{min-width:1260px}
7846        .cb-stats-rp-head,.cb-stats-rp-row{display:grid;grid-template-columns:minmax(115px,.75fr) minmax(230px,1.55fr) repeat(6,minmax(82px,.55fr)) minmax(105px,.7fr) minmax(105px,.7fr);align-items:center;gap:12px;padding:12px 16px}
7847        .cb-stats-rp-head{position:sticky;top:0;z-index:1;background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
7848        .cb-stats-rp-row{border-top:1px solid #edf2f7;color:#18314b;font-size:12px}
7849        .cb-stats-rp-row:nth-child(odd){background:#fbfdff}
7850        .cb-stats-rp-row strong{font-size:13px;color:#10233b}.cb-stats-rp-row small{display:block;margin-top:3px;color:#71839a;font-size:10px;line-height:1.3}
7851        .cb-stats-rp-number{font-weight:850;font-variant-numeric:tabular-nums}.cb-stats-rp-time{color:var(--cbs-accent);font-weight:900;font-variant-numeric:tabular-nums}
7852        .cb-stats-context{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;color:rgba(255,255,255,.78);font-size:11px;font-weight:800}
7853        .cb-stats-context span{display:inline-flex;align-items:center;min-height:27px;padding:0 9px;border:1px solid rgba(255,255,255,.18);border-radius:999px;background:rgba(255,255,255,.08)}
7854        .cb-stats-pulse{margin-top:18px;padding:22px;border:1px solid #dfe8f1;border-radius:12px;background:#fff}
7855        .cb-stats-pulse-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:16px}
7856        .cb-stats-pulse-head h2{margin:4px 0 0;font-size:24px;line-height:1.08}.cb-stats-pulse-head p{max-width:620px;margin:5px 0 0;color:#71839a;font-size:12px;line-height:1.45}
7857        .cb-stats-period{flex:0 0 auto;color:#526a82;font-size:10px;font-weight:900;letter-spacing:.05em;text-transform:uppercase}
7858        .cb-stats-decision-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
7859        .cb-stats-decision{position:relative;min-width:0;min-height:148px;padding:17px;border:1px solid #dfe8f1;border-radius:10px;background:#f8fbfd}
7860        .cb-stats-decision small{display:block;min-height:25px;color:#65798f;font-size:9px;font-weight:900;letter-spacing:.09em;text-transform:uppercase}
7861        .cb-stats-decision strong{display:block;margin-top:5px;color:#10253d;font-size:32px;line-height:1;font-variant-numeric:tabular-nums}.cb-stats-decision strong em{font-size:15px;font-style:normal}
7862        .cb-stats-decision p{margin:9px 0 0;color:#74869a;font-size:10.5px;line-height:1.4}
7863        .cb-stats-decision-status{display:inline-flex;align-items:center;gap:6px;margin-top:10px;color:#536c83;font-size:9px;font-weight:900;text-transform:uppercase}.cb-stats-decision-status:before{content:"";width:7px;height:7px;border-radius:50%;background:#8da0b3}
7864        .cb-stats-decision.is-good .cb-stats-decision-status:before{background:#16a06f}.cb-stats-decision.is-watch .cb-stats-decision-status:before{background:#e69c24}.cb-stats-decision.is-alert .cb-stats-decision-status:before{background:#df4050}
7865        .cb-stats-commercial-grid{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(360px,.9fr);gap:14px;margin-top:14px}
7866        .cb-stats-diagnosis{display:grid;gap:9px;margin-top:14px}
7867        .cb-stats-insight{display:grid;grid-template-columns:34px minmax(0,1fr);gap:11px;align-items:start;padding:13px;border:1px solid #e2eaf2;border-radius:10px;background:#fbfdff}
7868        .cb-stats-insight i{display:grid;place-items:center;width:32px;height:32px;border-radius:9px;background:#eaf3fb;color:var(--cbs-accent);font-style:normal;font-size:13px;font-weight:950}
7869        .cb-stats-insight strong{display:block;color:#18314b;font-size:12px}.cb-stats-insight span{display:block;margin-top:3px;color:#71839a;font-size:10.5px;line-height:1.4}
7870        .cb-stats-trend{display:grid;gap:10px;margin-top:16px}
7871        .cb-stats-trend-row{display:grid;grid-template-columns:58px minmax(80px,1fr) 48px;align-items:center;gap:9px;color:#526a82;font-size:10px;font-weight:850}
7872        .cb-stats-trend-track{height:24px;overflow:hidden;border-radius:7px;background:#eef3f7}.cb-stats-trend-fill{display:flex;align-items:center;justify-content:flex-end;min-width:4px;height:100%;box-sizing:border-box;padding-right:7px;border-radius:7px;background:linear-gradient(90deg,var(--cbs-dark),var(--cbs-accent));color:#fff;font-size:9px}
7873        .cb-stats-commercial-scroll{overflow-x:auto;margin-top:14px;border:1px solid #dfe8f1;border-radius:10px;background:#fff}
7874        .cb-stats-category-table{min-width:780px}.cb-stats-category-head,.cb-stats-category-row{display:grid;grid-template-columns:minmax(240px,1.5fr) repeat(4,minmax(100px,.65fr));align-items:center;gap:12px;padding:12px 15px}
7875        .cb-stats-category-head{background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.cb-stats-category-row{border-top:1px solid #edf2f7;color:#18314b;font-size:11px}.cb-stats-category-row strong{font-size:12px}.cb-stats-category-row small{display:block;margin-top:3px;color:#71839a;font-size:9.5px}
7876        .cb-stats-opportunity-table{min-width:800px}.cb-stats-opportunity-head,.cb-stats-opportunity-row{display:grid;grid-template-columns:minmax(260px,1.55fr) repeat(4,minmax(100px,.6fr));align-items:center;gap:12px;padding:12px 15px}
7877        .cb-stats-opportunity-head{background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.cb-stats-opportunity-row{border-top:1px solid #edf2f7;color:#18314b;font-size:11px}.cb-stats-opportunity-row strong{font-size:12px}.cb-stats-opportunity-row small{display:block;margin-top:3px;color:#71839a;font-size:9.5px;line-height:1.3}
7878        .cb-stats-visitor-table{min-width:1060px}.cb-stats-visitor-head,.cb-stats-visitor-row{display:grid;grid-template-columns:minmax(170px,1fr) minmax(200px,1.1fr) minmax(150px,.85fr) minmax(300px,1.65fr) minmax(105px,.62fr) minmax(145px,.8fr);align-items:center;gap:12px;padding:12px 15px}
7879        .cb-stats-visitor-head{background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.cb-stats-visitor-row{border-top:1px solid #edf2f7;color:#18314b;font-size:11px}.cb-stats-visitor-row strong{font-size:12px}.cb-stats-visitor-row small{display:block;margin-top:3px;color:#71839a;font-size:9.5px;line-height:1.3}
7880        .cb-stats-pagination{display:flex;align-items:center;justify-content:flex-end;gap:9px;margin-top:12px}.cb-stats-pagination span{color:#65798f;font-size:11px;font-weight:800}.cb-stats-pagination button{border:1px solid #cedbe8;border-radius:9px;padding:7px 11px;background:#fff;color:#183b5e;font:800 11px inherit;cursor:pointer}.cb-stats-pagination button:disabled{opacity:.4;cursor:not-allowed}
7881        .cb-stats-note{display:flex;align-items:flex-start;gap:10px;margin-top:14px;padding:13px 15px;border:1px solid #dfe8f1;border-radius:10px;background:#f8fbfd;color:#65798f;font-size:10.5px;line-height:1.45}.cb-stats-note strong{color:#27435f}
7882        .cb-stats-skeleton{overflow:hidden;position:relative;background:#e7edf4}
7883        .cb-stats-skeleton:after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent);animation:cbStatsShimmer 1.25s infinite}
7884        .cb-stats-loading{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:18px}
7885        .cb-stats-loading span{height:124px;border-radius:8px}
7886        @keyframes cbStatsShimmer{100%{transform:translateX(100%)}}
7887        @media(max-width:900px){
7888          .cb-stats-hero{grid-template-columns:1fr}
7889          .cb-stats-logo{max-width:360px}
7890          .cb-stats-kpis,.cb-stats-loading{grid-template-columns:repeat(2,minmax(0,1fr))}
7891          .cb-stats-grid{grid-template-columns:1fr}
7892          .cb-stats-decision-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
7893          .cb-stats-commercial-grid{grid-template-columns:1fr}
7894        }
7895        @media(max-width:600px){
7896          .cb-stats-page{padding:18px 14px 104px}
7897          .cb-stats-toolbar{align-items:stretch;flex-direction:column}.cb-stats-toolbar .cb-stats-back{align-self:flex-start}.cb-stats-export-actions{display:grid;grid-template-columns:1fr 1fr}.cb-stats-export-button{padding:0 10px}
7898          .cb-stats-hero{gap:20px;padding:25px 20px}
7899          .cb-stats-hero h1{font-size:30px}
7900          .cb-stats-logo{min-height:104px;padding:14px}
7901          .cb-stats-kpis,.cb-stats-loading{gap:8px}
7902          .cb-stats-kpi{min-height:112px;padding:15px}
7903          .cb-stats-kpi small{font-size:9px}
7904          .cb-stats-kpi strong{font-size:25px}
7905          .cb-stats-panel{padding:18px 15px}
7906          .cb-stats-panel-head{display:block}
7907          .cb-stats-quality{grid-template-columns:1fr}
7908          .cb-stats-quality-item{border-top:1px solid #e5ecf3;border-left:0}
7909          .cb-stats-overview-row{grid-template-columns:1fr 1fr;padding:15px}.cb-stats-overview-row>span:first-child,.cb-stats-overview-status{grid-column:1/-1}
7910          .cb-stats-pulse{padding:17px 14px}.cb-stats-pulse-head{display:block}.cb-stats-period{display:block;margin-top:9px}.cb-stats-decision-grid{gap:8px}.cb-stats-decision{min-height:138px;padding:14px}.cb-stats-decision strong{font-size:27px}
7911        }
7912      </style>
7913      <main class="cb-stats-page" style="--cbs-accent:{{ cbsAccent }};--cbs-dark:{{ cbsDark }};">
7914        <sc-if value="{{ cbsIsOverview }}" hint-placeholder-val="false">
7915          <div class="cb-stats-wrap">
7916            <button class="cb-stats-back" onClick="{{ cbsBack }}" aria-label="Volver a cabañas"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver a cabañas</button>
7917            <section class="cb-stats-hero"><div><span class="cb-stats-eyebrow">Rosgan Cabañas · Control integral</span><h1>Estadísticas de cabañas</h1><p>Seguimiento real de cada remate. Cada evento queda aislado por su ID de remate para evitar cruces de información.</p><div class="cb-stats-meta"><span>Registro activo por remate</span><span>Consulta consolidada</span></div></div></section>
7918            <section class="cb-stats-kpis" aria-label="Resumen de todos los remates"><article class="cb-stats-kpi"><small>Remates controlados</small><strong>{{ cbsOverviewAuctionCount }}</strong><span>Remates de cabaña disponibles.</span></article><article class="cb-stats-kpi"><small>Señales registradas</small><strong>{{ cbsOverviewEventCount }}</strong><span>Eventos acumulados sin mezclar remates.</span></article><article class="cb-stats-kpi"><small>Vistas</small><strong>{{ cbsOverviewViewCount }}</strong><span>Vistas de lote y de RP.</span></article><article class="cb-stats-kpi"><small>Videos</small><strong>{{ cbsOverviewVideoCount }}</strong><span>Reproducciones efectivas.</span></article><article class="cb-stats-kpi"><small>Contactos</small><strong>{{ cbsOverviewContactCount }}</strong><span>WhatsApp y llamadas.</span></article></section>
7919            <sc-if value="{{ cbsOverviewLoading }}" hint-placeholder-val="true"><div class="cb-stats-loading"><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span></div></sc-if>
7920            <sc-if value="{{ cbsOverviewReady }}" hint-placeholder-val="false"><section class="cb-stats-panel cb-stats-activity"><div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Detalle por remate</span><h2>Actividad registrada</h2><p>Abrí un remate para consultar catálogo, ranking y métricas individuales.</p></div></div><sc-if value="{{ cbsOverviewHasRows }}" hint-placeholder-val="true"><div class="cb-stats-overview-list"><sc-for list="{{ cbsOverviewRows }}" as="row"><button class="cb-stats-overview-row" onClick="{{ row.open }}"><span><small>REMATE {{ row.id }}</small><strong>{{ row.title }}</strong></span><span><small>SEÑALES</small><strong>{{ row.events }}</strong></span><span><small>VISTAS</small><strong>{{ row.views }}</strong></span><span><small>VIDEOS</small><strong>{{ row.videos }}</strong></span><span><small>CONTACTOS</small><strong>{{ row.contacts }}</strong></span><em class="cb-stats-overview-status {{ row.statusClass }}">{{ row.status }}</em></button></sc-for></div></sc-if><sc-if value="{{ cbsOverviewEmpty }}" hint-placeholder-val="false"><div class="cb-stats-empty">No hay remates disponibles.</div></sc-if><sc-if value="{{ cbsOverviewHasError }}" hint-placeholder-val="false"><div class="cb-stats-empty">{{ cbsOverviewError }}</div></sc-if></section></sc-if>
7921          </div>
7922        </sc-if>
7923        <sc-if value="{{ cbsIsAuction }}" hint-placeholder-val="true">
7924        <div class="cb-stats-wrap">
7925          <div class="cb-stats-toolbar">
7926            <button class="cb-stats-back" onClick="{{ cbsBack }}" aria-label="Volver al catálogo"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver al catálogo</button>
7927            <div class="cb-stats-export-actions"><button class="cb-stats-export-button" onClick="{{ cbsDownloadExcel }}" disabled="{{ cbsExportDisabled }}"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">{{ cbsExcelLabel }}</button><button class="cb-stats-export-button is-story" onClick="{{ cbsDownloadStory }}" disabled="{{ cbsExportDisabled }}"><img src="assets/icons/lucide/image-down.svg" alt="">{{ cbsStoryLabel }}</button></div>
7928          </div>
7929          <section class="cb-stats-hero">
7930            <div>
7931              <span class="cb-stats-eyebrow">Análisis comercial · {{ cbsTitle }}</span>
7932              <h1>Centro de estadísticas</h1>
7933              <p>{{ cbsTitle }}</p>
7934              <div class="cb-stats-context"><span>{{ cbsRpCount }} RPs</span><span>{{ cbsLotCount }} lotes</span><span>{{ cbsCategoryCount }} categorías</span><span>Actualización cada 10 segundos</span></div>
7935            </div>
7936            <sc-if value="{{ cbsHasLogo }}" hint-placeholder-val="{{ true }}"><div class="cb-stats-logo"><img data-dc-attr-src="{{ cbsLogo }}" alt="Cabaña principal"></div></sc-if>
7937          </section>
7938
7939          <sc-if value="{{ cbsLoading }}" hint-placeholder-val="{{ false }}">
7940            <div class="cb-stats-loading"><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span></div>
7941          </sc-if>
7942
7943          <sc-if value="{{ cbsReady }}" hint-placeholder-val="{{ true }}">
7944            <section class="cb-stats-pulse" aria-label="Pulso comercial del remate">
7945              <div class="cb-stats-pulse-head"><div><span class="cb-stats-section-label">Decisiones comerciales</span><h2>Pulso del interés comprador</h2><p>Indicadores relativos: muestran si el catálogo logra transformar atención en evaluación, difusión y consultas.</p></div><span class="cb-stats-period">Acumulado del remate</span></div>
7946              <sc-if value="{{ cbsAnalyticsLoading }}" hint-placeholder-val="false"><div class="cb-stats-loading"><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span></div></sc-if>
7947              <sc-if value="{{ cbsAnalyticsReady }}" hint-placeholder-val="true"><div class="cb-stats-decision-grid">
7948                <article class="cb-stats-decision {{ cbsContactHealthClass }}"><small>Conversión a consulta</small><strong>{{ cbsContactRate }}</strong><p>{{ cbsAnalyticsContactLabel }}
7948 sobre {{ cbsAnalyticsLotViews }} aperturas de lote.</p><span class="cb-stats-decision-status">{{ cbsContactHealth }}</span></article>
7949                <article class="cb-stats-decision {{ cbsDepthHealthClass }}"><small>Profundidad de evaluación</small><strong>{{ cbsEvaluationDepth }} <em>RPs</em></strong><p>Promedio de reproductores examinados por cada apertura de lote.</p><span class="cb-stats-decision-status">{{ cbsDepthHealth }}</span></article>
7950                <article class="cb-stats-decision {{ cbsVideoHealthClass }}"><small>Activación de video</small><strong>{{ cbsVideoRate }}</strong><p>{{ cbsAnalyticsVideoPlays }} reproducciones sobre {{ cbsAnalyticsRpViews }} vistas de RP.</p><span class="cb-stats-decision-status">{{ cbsVideoHealth }}</span></article>
7951                <article class="cb-stats-decision {{ cbsAttentionHealthClass }}"><small>Tiempo de evaluación</small><strong>{{ cbsAnalyticsAvgDuration }}</strong><p>Permanencia promedio efectiva al analizar un reproductor.</p><span class="cb-stats-decision-status">{{ cbsAttentionHealth }}</span></article>
7952              </div></sc-if>
7953            </section>
7954
7955            <div class="cb-stats-commercial-grid">
7956              <section class="cb-stats-panel">
7957                <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Lectura ejecutiva</span><h2>Qué está diciendo la demanda</h2><p>Hallazgos calculados sobre el comportamiento observado.</p></div></div>
7958                <div class="cb-stats-diagnosis"><sc-for list="{{ cbsInsights }}" as="item" hint-placeholder-count="3"><article class="cb-stats-insight"><i>{{ item.index }}</i><span><strong>{{ item.title }}</strong><span>{{ item.detail }}</span></span></article></sc-for></div>
7959              </section>
7960              <section class="cb-stats-panel">
7961                <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Evolución</span><h2>Interés por día</h2><p>Aperturas y evaluaciones; sirve para detectar cuándo empujar comunicación.</p></div></div>
7962                <sc-if value="{{ cbsHasTrend }}" hint-placeholder-val="true"><div class="cb-stats-trend"><sc-for list="{{ cbsTrend }}" as="item" hint-placeholder-count="6"><div class="cb-stats-trend-row"><span>{{ item.label }}</span><div class="cb-stats-trend-track"><div class="cb-stats-trend-fill" style="width:{{ item.width }};">{{ item.value }}</div></div><span>{{ item.intent }}</span></div></sc-for></div></sc-if>
7963                <sc-if value="{{ cbsNoTrend }}" hint-placeholder-val="false"><div class="cb-stats-empty">Todavía no hay una serie diaria suficiente.</div></sc-if>
7964              </section>
7965            </div>
7966
7967            <section class="cb-stats-panel cb-stats-activity">
7968              <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Demanda por segmento</span><h2>Rendimiento comercial por categoría</h2><p>Compara interés por RP, uso de video, señales de intención y tiempo de evaluación. No premia a una categoría solamente por tener más animales.</p></div></div>
7969              <sc-if value="{{ cbsHasCategoryPerformance }}" hint-placeholder-val="true"><div class="cb-stats-commercial-scroll"><div class="cb-stats-category-table"><div class="cb-stats-category-head"><span>Categoría</span><span>Vistas / RP</span><span>Video / vista</span><span>Intención</span><span>Tiempo prom.</span></div><sc-for list="{{ cbsCategoryPerformance }}" as="item" hint-placeholder-count="4"><article class="cb-stats-category-row"><span><strong>{{ item.label }}</strong><small>{{ item.published }} RPs publicados</small></span><span><strong>{{ item.viewsPerRp }}</strong><small>{{ item.views }} evaluaciones</small></span><span><strong>{{ item.videoRate }}</strong><small>{{ item.videos }} reproducciones</small></span><span><strong>{{ item.intent }}</strong><small>contactos + compartidos</small></span><span><strong>{{ item.avgTime }}</strong><small>por medición</small></span></article></sc-for></div></div></sc-if>
7970              <sc-if value="{{ cbsNoCategoryPerformance }}" hint-placeholder-val="false"><div class="cb-stats-empty">Todavía no hay actividad suficiente para comparar categorías.</div></sc-if>
7971            </section>
7972
7973            <section class="cb-stats-panel cb-stats-activity">
7974              <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Acción por reproductor</span><h2>Oportunidades prioritarias</h2><p>Los RPs con mayor atención, ordenados para decidir dónde reforzar difusión o impulsar una consulta.</p></div><strong>{{ cbsOpportunityCount }} analizados</strong></div>
7975              <sc-if value="{{ cbsHasOpportunities }}" hint-placeholder-val="true"><div class="cb-stats-commercial-scroll"><div class="cb-stats-opportunity-table"><div class="cb-stats-opportunity-head"><span>RP y lote</span><span>Vistas RP</span><span>Video / vista</span><span>Tiempo prom.</span><span>Intención</span></div><sc-for list="{{ cbsOpportunities }}" as="item" hint-placeholder-count="8"><article class="cb-stats-opportunity-row"><span><strong>{{ item.rp }} · {{ item.lot }}</strong><small>{{ item.category }}</small></span><span><strong>{{ item.rpViews }}</strong><small>evaluaciones</small></span><span><strong>{{ item.videoRate }}</strong><small>{{ item.videos }} plays</small></span><span><strong>{{ item.avgTime }}</strong><small>{{ item.samplesLabel }}</small></span><span><strong>{{ item.intent }}</strong><small>acciones</small></span></article></sc-for></div></div></sc-if>
7976              <sc-if value="{{ cbsNoOpportunities }}" hint-placeholder-val="false"><div class="cb-stats-empty">Todavía no hay actividad individual registrada por RP.</div></sc-if>
7977            </section>
7978
7979            <section class="cb-stats-panel cb-stats-activity">
7980              <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Audiencia identificada</span><h2>Visitantes del remate</h2><p>Nombre, correo y teléfono de usuarios registrados; quienes navegan sin iniciar sesión se muestran como Anónimo.</p></div><strong>{{ cbsVisitorCount }} visitantes</strong></div>
7981              <sc-if value="{{ cbsHasVisitors }}" hint-placeholder-val="true"><div class="cb-stats-commercial-scroll"><div class="cb-stats-visitor-table"><div class="cb-stats-visitor-head"><span>Nombre y apellido</span><span>Correo electrónico</span><span>Teléfono</span><span>Lotes vistos</span><span>Tiempo total</span><span>Última visita</span></div><sc-for list="{{ cbsVisitors }}" as="visitor" hint-placeholder-count="6"><article class="cb-stats-visitor-row"><span><strong>{{ visitor.name }}</strong></span><span><strong>{{ visitor.email }}</strong></span><span><strong>{{ visitor.phone }}</strong></span><span><strong>{{ visitor.lots }}</strong><small>{{ visitor.detail }}</small></span><span><strong>{{ visitor.totalTime }}</strong><small>{{ visitor.activity }}</small></span><span><strong>{{ visitor.lastSeen }}</strong></span></article></sc-for></div></div><div class="cb-stats-pagination"><button onClick="{{ cbsVisitorPrevious }}" disabled="{{ cbsVisitorPreviousDisabled }}">Anterior</button><span>Página {{ cbsVisitorPage }} de {{ cbsVisitorPageCount }} · 25 por página</span><button onClick="{{ cbsVisitorNext }}" disabled="{{ cbsVisitorNextDisabled }}">Siguiente</button></div></sc-if>
7982              <sc-if value="{{ cbsNoVisitors }}" hint-placeholder-val="false"><div class="cb-stats-empty">Todavía no hay visitantes registrados para este remate.</div></sc-if>
7983            </section>
7984
7985            <div class="cb-stats-note"><strong>Cómo leerlo:</strong><span>“Intención” suma contactos y compartidos. Cada visitante registrado se identifica mediante su sesión validada; las visitas sin inicio de sesión se conservan como Anónimo.</span></div>
7986          </sc-if>
7987        </div>
7988        </sc-if>
7989      </main>
7990    </sc-if>
7991
7992    <!-- MOBILE CARGAR LOTE FAB -->
7993    <sc-if value="{{ lotDestinationOpen }}" hint-placeholder-val="false">
7994      <div class="lot-destination-backdrop" onKeyDown="{{ lotDestinationKey }}">
7995        <section class="lot-destination-dialog" role="dialog" aria-modal="true" aria-label="Elegir dónde cargar el lote">
7996          <div class="lot-destination-heading"><div><small>PUBLICÁ TU PRÓXIMO LOTE</small><h2>Elegí el destino</h2></div><button class="lot-destination-close" autofocus onClick="{{ closeLotDestination }}" aria-label="Cerrar selector">✕</button></div>
7997          <p class="lot-destination-intro">Elegí uno de los destinos habilitados para tu cuenta.</p>
7998          <sc-if value="{{ lotDestinationRoslot }}"><button class="lot-destination-card destination-roslot" onClick="{{ chooseRoslotDestination }}"><span class="destination-roslot-icon"><img src="/assets/icons/lucide/tag.svg" alt=""></span><span class="destination-content"><strong>Roslot</strong><small>Publicá en el mercado digital de Rosgan</small></span><img class="destination-arrow" src="/assets/icons/lucide/arrow-up-right.svg" alt=""></button></sc-if>
7999          <sc-if value="{{ lotDestinationLoading }}"><p>Consultando remates habilitados…</p></sc-if>
8000          <sc-if value="{{ lotDestinationError }}"><p role="alert">{{ lotDestinationError }}</p><button onClick="{{ goLotDestination }}">Reintentar</button></sc-if>
8001          <sc-for list="{{ lotDestinationChoices }}" as="destination"><button class="lot-destination-card" onClick="{{ destination.choose }}"><span class="destination-content"><span class="destination-type">{{ destination.type }}</span><strong>{{ destination.title }}</strong><span class="destination-logos"><sc-for list="{{ destination.logos }}" as="brand"><span class="destination-logo" title="{{ brand.name }}" style="background:{{ brand.color }};"><img data-dc-attr-src="{{ brand.logo }}" alt="{{ brand.name }}"></span></sc-for></span></span><img class="destination-arrow" src="/assets/icons/lucide/arrow-up-right.svg" alt=""></button></sc-for>
8002          <sc-if value="{{ lotDestinationEmpty }}"><p>No tenés remates habilitados para cargar lotes en este momento.</p></sc-if>
8003          <p class="lot-destination-note">Solo ves los destinos disponibles para tu cuenta.</p>
8004        </section>
8005      </div>
8006    </sc-if>
8007    <sc-if value="{{ showLoadFab }}" hint-placeholder-val="{{ false }}">
8008      <button class="app-mobile-load-fab" onClick="{{ goMobileLoad }}" style="position:fixed;
8008 right:18px; bottom:calc(72px + env(safe-area-inset-bottom)); z-index:55; display:inline-flex; align-items:center; gap:9px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:14px 22px; font-size:14.5px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; box-shadow:0 12px 30px rgba(230,57,70,0.5); animation:bidPulse 2.4s infinite;"><span style="font-size:19px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button>
8009    </sc-if>
8010
8011    <!-- MOBILE BOTTOM NAV -->
8012    <sc-if value="{{ compactNavigation }}" hint-placeholder-val="{{ false }}">
8013      <nav class="app-mobile-bottom-nav" aria-label="Navegación principal" style="position:fixed; bottom:0; left:0; right:0; background:rgba(255,255,255,0.94); backdrop-filter:blur(14px); border-top:1px solid #dfe7f0; display:flex; justify-content:space-around; padding:10px 0 max(14px, env(safe-area-inset-bottom)); z-index:50;">
8014        <sc-for list="{{ mobileNavItems }}" as="n" hint-placeholder-count="5">
8015          <button type="button" class="app-mobile-nav-item" onClick="{{ n.go }}" aria-current="{{ n.current }}" style="color:{{ n.mobColor }};"><span class="app-mobile-nav-icon" style="background:{{ n.iconBg }};"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M0 0" data-dc-svg-d="{{ n.mobilePath }}"></path></svg></span><span class="app-mobile-nav-label">{{ n.label }}</span></button>
8016        </sc-for>
8017      </nav>
8018    </sc-if>
8019
8020    <sc-if data-public-blocked-overlay value="{{ profilePhotoViewerOpen }}" hint-placeholder-val="{{ false }}">
8021      <div style="position:fixed; inset:0; z-index:140; display:grid; place-items:center; padding:24px; background:rgba(0,14,32,0.86);
8021 backdrop-filter:blur(7px);">
8022        <figure style="position:relative; width:min(880px, 94vw); height:min(82vh, 880px); margin:0; display:grid; place-items:center;">
8023          <img data-dc-attr-src="{{ profilePhotoViewerUrl }}" alt="{{ profilePhotoViewerAlt }}" style="display:block; max-width:100%; max-height:100%; width:auto; height:auto; object-fit:contain; border-radius:8px; box-shadow:0 24px 80px rgba(0,0,0,0.46); background:#fff;">
8024          <button onClick="{{ closeProfilePhotoViewer }}" title="Cerrar foto" aria-label="Cerrar foto" style="position:absolute; top:0; right:0; width:42px; height:42px; border:1px solid rgba(255,255,255,0.45); border-radius:50%; background:rgba(0,29,61,0.82); display:grid; place-items:center; cursor:pointer; box-shadow:0 8px 24px rgba(0,0,0,0.25);">
8025            <img src="assets/icons/lucide/x.svg" alt="" style="width:20px; height:20px; filter:brightness(0) invert(1);">
8026          </button>
8027        </figure>
8028      </div>
8029    </sc-if>
8030
8031    <!-- TOAST -->
8032    <sc-if value="{{ toast }}" hint-placeholder-val="{{ false }}">
8033      <div style="position:fixed; bottom:{{ toastBottom }}; left:50%; transform:translateX(-50%); background:#001d3d; color:#fff; padding:12px 22px; border-radius:100px; font-size:13.5px; font-weight:600; box-shadow:0 14px 40px rgba(0,20,45,0.4); z-index:60; animation:floatIn 300ms both; white-space:nowrap; max-width:90vw; overflow:hidden; text-overflow:ellipsis;">{{ toast }}</div>
8034    </sc-if>
8035    <sc-if value="{{ showRoslotLiveToast }}" hint-placeholder-val="{{ false }}">
8036      <button class="roslot-event-toast" onClick="{{ openRoslotLiveToast }}" aria-live="assertive" aria-label="{{ roslotLiveToastAria }}">
8037        <span class="roslot-event-toast-icon"><img data-dc-attr-src="{{ roslotLiveToastIcon }}" alt=""></span>
8038        <span class="roslot-event-toast-copy"><strong>{{ roslotLiveToastTitle }}</strong><span>{{ roslotLiveToastBody }}</span></span>
8039        <span class="roslot-event-toast-arrow">›</span>
8040      </button>
8041    </sc-if>
8042  </div>
8043</sc-if>
8044</x-dc>
8045<script src="/assets/rosgan-tv-editor.js"></script>
vendor: 1 bytes, line 8045
8045
8046<script src="/assets/rosgan-tv-health.js"></script>
vendor: 1 bytes, line 8046
8046
8047<script src="/assets/rosgan-tv-multiview.js"></script>
vendor: 1 bytes, line 8047
8047
8048<script src="/assets/rosgan-airtime.js"></script>
vendor: 1 bytes, line 8048
8048
8049<script src="/assets/rosgan-order-command.js"></script>
vendor: 1 bytes, line 8049
8049
8050<script src="/assets/rosgan-sales-order-import.js"></script>
vendor: 1 bytes, line 8050
8050
8051<script src="/assets/rosgan-preorder-excel.js?v=2"></script>
vendor: 1 bytes, line 8051
8051
8052<script src="/assets/rosgan-audience.js"></script>
vendor: 1 bytes, line 8052
8052
8053<script src="/assets/rosgan-price-input.js"></script>
vendor: 1 bytes, line 8053
8053
8054<script src="/assets/rosgan-billing.js"></script>
8054
8055<link rel="stylesheet" href="/assets/rosgan-auditor.css?v=upload-timeline-1">
8056<script src="/assets/rosgan-payment-term.js?v=slash-days-1"></script>
vendor: 1 bytes, line 8056
8056
8057<script src="/assets/rosgan-lot-pdf.js"></script>
vendor: 1 bytes, line 8057
8057
8058<script src="/assets/rosgan-auditor-excel.js"></script>
vendor: 1 bytes, line 8058
8058
8059<script src="/assets/catalogo/news-pages.js?v=1"></script>
8059<script src="/assets/catalog-cover.js?v=news-pages-1"></script>
vendor: 1 bytes, line 8059
8059
8060<script src="/assets/rosgan-auditor.js?v=auditor-filters-2"></script>
vendor: 1 bytes, line 8060
8060
8061<script src="/assets/legacy-comitentes.js"></script>
vendor: 1 bytes, line 8061
8061
8062<script src="/assets/comitente-merge.js"></script>
vendor: 1 bytes, line 8062
8062
8063<script src="/assets/dev-activity.js"></script>
vendor: 1 bytes, line 8063
8063
8064<script src="/assets/dev-whatsapp.js"></script>
vendor: 1 bytes, line 8064
8064
8065<script src="/assets/dev-activity-page.js"></script>
vendor: 1 bytes, line 8065
8065
8066<script src="/assets/comitente-alias.js"></script>
vendor: 1 bytes, line 8066
8066
8067<script src="/assets/home-channel.js"></script>
vendor: 1 bytes, line 8067
8067
8068<script src="/assets/home-spot-player.js"></script>
8068
8069<style>
8070.rg-est-status .dc-lucide-circle-check{--dc-icon:url('/assets/icons/lucide/circle-check.svg')}
8071.rg-est-status .dc-lucide-triangle-alert{--dc-icon:url('/assets/icons/lucide/triangle-alert.svg')}
8072</style>
8073<style>
8074.rg-est-picker{position:relative;min-width:0}.rg-est-trigger{width:100%;min-height:46px;border:1px solid #ccdae8;border-radius:12px;background:#fff;color:#102f50;padding:9px 12px;display:flex;align-items:center;gap:9px;text-align:left;font:13px inherit;cursor:pointer}.rg-est-trigger:disabled{background:#f5f8fc;color:#7b8ba0;cursor:default}.rg-est-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rg-est-status{width:28px;height:28px;flex:0 0 28px;display:grid;place-items:center;border-radius:8px}.rg-est-status .dc-lucide{width:18px;height:18px}.rg-est-menu{position:absolute;z-index:45;left:0;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #d4e0ec;border-radius:14px;box-shadow:0 16px 34px #00264e26;padding:8px}.rg-est-search{width:100%;box-sizing:border-box;min-height:42px;border:1px solid #dbe5ef;border-radius:9px;padding:9px 11px;font:14px inherit;color:#123956;background:#f7faff}.rg-est-options{max-height:min(300px,42dvh);overflow:auto;margin-top:6px;overscroll-behavior:contain}.rg-est-option{width:100%;display:flex;align-items:center;gap:10px;border:0;border-radius:9px;background:#fff;text-align:left;min-height:54px;padding:9px 8px;color:#173753;cursor:pointer;font:13px inherit}.rg-est-option:hover,.rg-est-option:focus-visible,.rg-est-option[aria-selected=true]{background:#edf5fd}.rg-est-option-text{display:grid;gap:3px;min-width:0}.rg-est-option strong{font-size:12px;overflow-wrap:anywhere}.rg-est-option small{font-size:10px;color:#60758c;line-height:1.4}.rg-est-count{margin:7px 4px 2px;color:#718296;font-size:10px}.rg-est-more{width:100%;min-height:38px;border:0;border-radius:8px;background:#edf5fd;color:#075da8;cursor:pointer;font:12px inherit}
8075</style>
8076<script src="/assets/cabana-lot-identity.js"></script>
8076
8077<link rel="stylesheet" href="/assets/cabana-prebid.css">
8078<script src="/assets/cabana-prebid.js?v=active-badge-3"></script>
vendor: 1 bytes, line 8078
8078
8079<script src="/assets/prebid-start-countdown.js?v=1"></script>
8079
8080<link rel="stylesheet" href="/assets/prebid-active-badge.css?v=3">
8081<link rel="stylesheet" href="/assets/cabana-commercial-terms.css?v=2">
8082<script src="/assets/cabana-commercial-terms.js?v=2"></script>
vendor: 1 bytes, line 8082
8082
8083<script src="/assets/cabana-installments.js"></script>
vendor: 1 bytes, line 8083
8083
8084<script src="/assets/cabana-prebid-report.js"></script>
8084
8085<link rel="stylesheet" href="/assets/cabana-prebid-report.css">
8086<script src="/assets/cabana-media-url.js"></script>
vendor: 1 bytes, line 8086
8086
8087<script src="/assets/rosgan-sale-divisions.js"></script>
vendor: 1 bytes, line 8087
8087
8088<script src="/assets/rosgan-operations-patch.js"></script>
vendor: 1 bytes, line 8088
8088
8089<script src="/assets/rosgan-weighted-prices.js"></script>
vendor: 1 bytes, line 8089
8089
8090<script src="/assets/price-board.js"></script>
vendor: 1 bytes, line 8090
8090
8091<script src="/assets/auction-share.js?v=quantity-weight-range-2"></script>
vendor: 1 bytes, line 8091
8091
8092<script src="/assets/rosgannet-price-results.js"></script>
vendor: 1 bytes, line 8092
8092
8093<script src="/assets/auction-results.js"></script>
vendor: 1 bytes, line 8093
8093
8094<script src="/assets/statistics/dashboard.js"></script>
vendor: 1 bytes, line 8094
8094
8095<script src="/assets/statistics/historical.js"></script>
vendor: 1 bytes, line 8095
8095
8096<script src="/assets/rosgan-sale-divisions-editor.js"></script>
vendor: 1 bytes, line 8096
8096
8097<script src="/assets/rosgan-commercial-progress.js"></script>
vendor: 1 bytes, line 8097
8097
8098<script src="/assets/feria-flyer.js"></script>
vendor: 1 bytes, line 8098
8098
8099<script src="/assets/feria-thumbnail.js"></script>
vendor: 1 bytes, line 8099
8099
8100<script src="/assets/feria-thumbnail-preview.js"></script>
vendor: 1 bytes, line 8100
8100
8101<script src="/assets/feria-devices.js"></script>
vendor: 1 bytes, line 8101
8101
8102<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;: {&quot;width&quot;: 1280, &quot;height&quot;: 900}, &quot;iniciarEnHome&quot;: {&quot;editor&quot;: &quot;boolean&quot;, &quot;default&quot;: false, &quot;tsType&quot;: &quot;boolean&quot;, &quot;section&quot;: &quot;Comportamiento&quot;}, &quot;animaciones&quot;: {&quot;editor&quot;: &quot;boolean&quot;, &quot;default&quot;: true, &quot;tsType&quot;: &quot;boolean&quot;, &quot;section&quot;: &quot;Comportamiento&quot;}}">
8103const ARGENTINA_PROVINCES = [
8104  'Buenos Aires', 'Catamarca', 'Chaco', 'Chubut', 'Ciudad Autónoma de Buenos Aires',
8105  'Córdoba', 'Corrientes', 'Entre Ríos', 'Formosa', 'Jujuy', 'La Pampa', 'La Rioja',
8106  'Mendoza', 'Misiones', 'Neuquén', 'Río Negro', 'Salta', 'San Juan', 'San Luis',
8107  'Santa Cruz', 'Santa Fe', 'Santiago del Estero',
8108  'Tierra del Fuego, Antártida e Islas del Atlántico Sur', 'Tucumán'
8109];
8110
8111class Component extends DCLogic {
8112  state = {
8113    publicAdvanceMode: window.__ROSGAN_PUBLIC_ADVANCE__ || '',
8114    route: 'login', authMode: 'signin', formName: '', formEmail: '', formPassword: '', formPhone: '+54', authPasswordVisible: false,
8115    accountAssistantOpen: false, accountAssistantMethod: '', accountAssistantStep: 0, accountAssistantMessage: '',
8116    authMessage: '', authReady: false, authWelcomeRequested: false, user: null, isMobile: window.innerWidth < 760, compactNavigation: window.innerWidth <= 1024,
8117    launchInterest: null, launchInterestLoading: false, launchInterestSaving: false, launchInterestMessage: '',
8118    launchInterestsAdmin: null, launchInterestsAdminTotal: 0, launchInterestsAdminLoading: false, launchInterestsAdminError: '',
8119    lotYieldCategories: null, lotYieldLoading: false, lotYieldError: '', lotYieldSavingId: '', lotYieldMessage: null,
8120    emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '',
8121    phoneVerificationRequired: false, phoneVerificationStep: 'phone', phoneVerificationNumber: '+54', phoneVerificationCode: '', phoneVerificationBusy: false, phoneVerificationMessage: '',
8122    ferias: null, consignatarias: null, videos: null, cafe: null,
8123    parallax: 0, statProgress: 0, toast: '',
8124    notificationsStatus: 'idle', notificationsMessage: '', notificationsOpen: false,
8125    whatsappNotificationsEnabled: false, whatsappNotificationsAvailable: false, whatsappNotificationsLoading: false, whatsappNotificationsSaving: false, whatsappNotificationsMessage: '',
8126    notificationsItems: null, notificationsLoading: false, notificationsError: '',
8127    notificationHistory: null, notificationHistoryLoading: false, notificationHistoryError: '',
8128    roslotMobilePanelOpen: false, roslotLiveToast: null, roslotLivePulseId: '',
8129    monthRemates: null, cafeVideos: null, heroSlide: 0, homeHeroSlide: 0, homeChannelManual: null, homeChannelNow: 0, homeSpots: [], homeSpotIndex: 0, homeSpotFailures: [],
8130    profileFerias: [], profileFeriasLoading: false, profileFeriasError: '', profileFeriasUid: '',
8131    profileRosganLots: [], profileRosganLoading: false, profileRosganError: '', profileRosganScopeUid: '',
8132    lots: null, lotsLoading: false, lotsReal: false, myLots: null, myLotsLoading: false, myLotsError: '',
8133    comitentes: null, comitentesLoading: false, comitentesError: '', comitenteForm: null, comitenteFormOpen: false, comitenteSaving: false, comitenteLookupLoading: false, comitenteFormMessage: null,
8134    establishments: null, establishmentsLoading: false, establishmentsError: '', estForm: null, estFormOpen: false, estSaving: false, estLookupLoading: false, estFormMessage: null, estDeleting: '',
8135    estPlaceLoading: false, estPlaceResults: [], estPlaceError: '',
8136    poolQuery: '', poolStatus: 'today', poolSort: 'recientes', poolView: 'grid', filtersOpen: false, marketClock: Date.now(),
8137    poolFilters: { category: [], destination: [], fattening: [], province: [], china: false },
8138    roslotDailyReport: null, roslotDailyReportLoading: false, roslotDailyReportError: '', roslotDailyReportDate: '', roslotDailyReportFilter: 'all', roslotDailyReportSearch: '',
8139    roslotPresence: null, roslotPresenceLoading: false, roslotPresenceError: false,
8140    selectedCert: '', selectedVideo: 0, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true, videoThumbByUrl: {}, videoThumbFailedByUrl: {}, bidByCert: {}, bidsPlaced: {}, bidMessage: null, bidSubmitting: false, quickBidCert: '', lotClosingCert: '', closedRoslotLots: {}, roslotBidSuccessFx: null,
8141    lotBids: {}, lotBidsLoading: {}, myBids: null, myBidsLoading: false, myBidsError: '', pendingBids: null, pendingBidsLoading: false, pendingBidsError: '', bidActioning: '',
8142    viewsBump: {}, lotViewCounts: {}, lotViewLoading: {},
8143    loadStep: 0, form: null, formMessage: null, publishing: false, vidProg: {},
8144    rosganLotStep: 0, rosganLotForm: null, rosganLotRemates: null, rosganLotCategories: null, rosganLotBreeds: null,
8145    rosganLotOptions: null, rosganLotOptionsLoading: false, rosganLotOptionsError: '',
8146    rosganBreedSearch: '', rosganActiveBreedFamily: '', rosganBreedCrosses: {},
8147    rosganLotConsignatarias: null, rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null,
8148    rosganLotLoading: false, rosganLotRelationsLoading: false, rosganLotPublishing: false, rosganLotMessage: null, rosganCreatedLot: null, rosganEditingLot: null, rosganConsignatariaMenuOpen: false,
8149    rosganComitenteFormOpen: false, rosganComitenteSaving: false, rosganComitenteLookupLoading: false, rosganComitenteMessage: null,
8150    rosganComitenteSearch: '', rosganComitenteMenuOpen: false,
8151    rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' },
8152    rosganEstablishmentFormOpen: false, rosganEstablishmentSaving: false, rosganEstablishmentMessage: null,
8153    rosganEstablishmentForm: { nombre: '', provincia: '', localidad: '', renspa: '', latitud: '', longitud: '' },
8154    rosganEstPlaceLoading: false, rosganEstPlaceResults: [], rosganEstPlaceError: '',
8155    editLotCert: '', editOriginalForm: null, editDeletedVideoIds: [], editLotLoading: false,
8156    profileTab: 'resumen', dashProgress: 0,
8157    buyerAuths: null, buyerAuthsLoading: false, buyerAuthsError: '', buyerAccess: null, buyerAccessLoading: false, buyerAccessError: '', sellerLabels: {}, buyerAccounts: null, buyerAccountsLoading: false, buyerAccountsError: '', buyerDirectorySearch: '', authSyncError: '', baForm: null, baFormOpen: false, baSaving: false, baFormMessage: null, baActioning: '', sellerClosedPage: 1, buyerClosedPage: 1,
8158    creditSummaryOpen: false, creditSummaryLoading: false, creditSummaryError: '', creditSummary: null, creditSummaryAuthorization: null, creditSummaryPerspective: 'buyer',
8159    creditSummaryCache: {}, creditSummaryLoadingIds: {}, creditSummaryErrors: {},
8160    creditHistoryOpen: false, creditHistoryPerspective: 'buyer', creditHistoryAuthorization: null,
8161    republishLot: null, republishForm: null, republishSaving: false, republishMessage: null,
8162    profileFetchInfo: {}, apiUser: null, apiSessionError: '', userConsignataria: null, userConsignatarias: [], lotSellerConsignatarias: {},
8163    profileEditorOpen: false, profileEditorSaving: false, profileEditorError: '',
8164    profileEditorForm: { firstName: '', lastName: '', phoneNumber: '' }, profilePhotoPreview: '', profilePhotoFailed: false, profilePhotoViewerOpen: false,
8165    profileCropOpen: false, profileCropSource: '', profileCropZoom: 1, profileCropX: 50, profileCropY: 50,
8166    adminUsersOpen: false, adminUsers: null, adminUsersLoading: false, adminUsersError: '', adminUsersSearch: '', adminUsersGroup: 'recent', adminUsersConsignataria: '', adminUsersPage: 1, adminCompanyMenuOpen: false,
8167    adminSelectedUser: null, adminSelectedRoles: [], adminSelectedConsignatariaIds: [], adminConsignatarias: [], adminUserRelationLoading: false,
8168    adminOriginalRoles: [], adminOriginalConsignatariaIds: [], adminOriginalRelationIsActive: false,
8169    adminUserSaving: false, adminUserMessage: null,
8170    mailingOpen: false, mailingLoading: false, mailingBusy: false, mailingError: '', mailingMessage: null,
8171    mailingStatus: null, mailingContacts: [], mailingContactTotal: 0, mailingLaunchInterestTotal: 0, mailingCampaigns: [], mailingSelectedId: '', mailingConsentConfirmed: false,
8172    mailingContactsState: 'idle', mailingCampaignsState: 'idle', mailingStatusState: 'idle', mailingLaunchState: 'idle',
8173    mailingReport: null, mailingReportLoading: false, mailingReportError: '', mailingReportExporting: false,
8174    mailingReportSearch: '', mailingReportStatusFilter: 'all', mailingReportLetter: 'all', mailingReportPage: 1,
8175    mailingEditorExpanded: false, mailingEditorSourceMode: false, mailingInsertDialog: '', mailingInsertUrl: '', mailingInsertText: '', mailingInsertImageSize: 'medium', mailingInsertImageLink: '', mailingImageUploading: false, mailingImageError: '',
8176    mailingDraft: { name: '', subject: '', previewText: '', html: '', text: '', audience: 'all', campaignType: 'standard' }, mailingTestEmail: '',
8177    remates: null, remateYears: null, rematesLoading: false, remateArchiveOpen: false, remateArchiveLimit: 12, remateYear: new Date().getFullYear(), remateType: '', carSlide: 0,
8178    rosganRemates: null, rosganRematesLoading: false, rosganRematesError: '',
8179    rematePanelPickerOpen: false, rematePanelPickerLoading: false, rematePanelPickerError: '', rematePanelScopeUid: '', rematePanelItems: [], rematePanelSelectedId: '',
8180    rosganRemateTypes: null, rosganRemateTypesLoading: false, rosganRemateTypesError: '',
8181    rosganRemateFormOpen: false, rosganRemateEditingId: '', rosganRemateForm: null,
8182    rosganRemateSaving: false, rosganRemateDeleting: '', rosganRemateMessage: null,
8183    rosganRemateFlyer: null, rosganRemateFlyerPreview: '',
8184    currentRemate: null, remateLots: null, remateLotsLoading: false, remateLotsReal: false, rdQuery: '', rdSort: 'orden', rdView: 'grid', rdLightbox: null,
8185    rosganOpsDrafts: {}, rosganOpsConfig: null, rosganSalesOrderRatings: {}, rosganSalesOrderSchedule: null, rosganOpsPulse: 0, rosganOpsLoading: false, rosganTvQuery: '', rosganOfficialPrices: {},
8186    remateLotAlerts: {}, remateLotAlertsLoading: false, remateLotAlertBusy: '',
8187    rosganSalesOrderLots: [], rosganSalesOrderSaving: false, rosganSalesOrderImporting: false, rosganSalesOrderError: '', rosganSalesOrderSaved: false, rosganSalesOrderVideoCert: '',
8188    rosganSalesOrderQuery: '', rosganSalesOrderStatusFilter: '', rosganSalesOrderCategoryFilter: '', rosganSalesOrderConsignatariaFilter: '', rosganSalesOrderRatingFilter: '', rosganSalesOrderRatingSnapshot: null,
8189    rosganLotVideoState: {}, rosganLotVideoDeleting: '',
8190    cabanas: null, cabanasLoading: false, cabanasError: '', cabanasCollectionReady: false, cabanaCardMedia: {}, cabanaBreed: '',
8191    currentCabana: null, cabanaLots: null, cabanaLotsLoading: false, cbQuery: '', cbCat: 'todos', cabanaAnalyticsSummary: null, cabanaAnalyticsLoading: false, cabanaStatsOverview: false, cabanaStatsOverviewRows: [], cabanaStatsOverviewLoading: false, cabanaStatsOverviewError: '', cabanaPublicDeepLoading: false, cabanaBrandPreparing: false, cabanaCardVideoUploads: {},
8192    cabanaStatsExportBusy: '', cabanaStatsVisitorPage: 1,
8193    cabanaSalesOrderOpen: false, cabanaSalesOrderGrouped: false, cabanaSalesOrderLots: [], cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false,
8194    cabanaCommercialDrafts: {}, cabanaCommercialFilter: 'todos', cabanaCommercialView: 'carga', cabanaCommercialOpenRow: '', cabanaCommercialLoading: false,
8195    cabanaTvLotKey: '', cabanaTvPulse: 0, cabanaTvConfig: null, cabanaTvConfigPulse: 0, cabanaTvClock: 0, cabanaTvCycleStartedAt: 0, cabanaTvVideoIndex: 0,
8196    cabanaTvWeather: null, cabanaTvWeatherLocation: '', cabanaTvWeatherLoading: false, cabanaTvWeatherError: '',
8197    currentCabanaLot: null, currentCabanaLotVariants: [], cbLotVideo: 0, cbCarouselStep: 0, cbCarouselOffsets: {}, cbCarouselFading: false, cbCarouselFadingKey: '', cbCarouselCrossfadeActive: false, cbCarouselPreviousMedia: {}, cabanaLotVideosLoading: false, cabanaLotMediaReady: false, cabanaLotMediaFailed: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true, cabanaLotQuickVideoUploading: false, cabanaLotQuickVideoProgress: 0, cabanaLotQuickVideoStage: '', cabanaLotVideoDeleting: '',
8198    cabanaPrebids: [], cabanaPrebidMine: [], cabanaPrebidsLoading: false, cabanaPrebidsError: '', cabanaPrebidModalOpen: false, cabanaPrebidSubmitting: false, cabanaPrebidMessage: null, cabanaPrebidRolling: false, cabanaPrebidSuccessFx: null,
8199    cabanaPrebidPanelAllowed: false, cabanaPrebidPanelOpen: false, cabanaPrebidPanelLoading: false, cabanaPrebidPanelError: '', cabanaPrebidPanelData: null, cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all', cabanaPrebidPanelLot: '',
8200    cabanaPrebidForm: { cuitCuil: '', amount: '', selectedIdentifiers: [], enComision: false, enComisionPara: '' },
8201    cabanaLotStoryBusy: false,
8202    cabanaPriceStoryBusy: false,
8203    cabanaSocialBumps: {},
8204    cabanaLotCreateRemate: null, cabanaLotCreateForm: null, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateSaving: false, cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null, cabanaLotDeleting: '',
8205    remateConfigForm: null, remateConfigToggles: null, remateConfigSaved: false, remateConfigEditingId: '', remateConfigSaving: '',
8206    feriaTab: 'todos', feriaFeatured: null, feriaFormOpen: false, feriaSubmitting: false, feriaFormMessage: null, feriaCreated: [],
8207    feriaForm: { consignataria: '', provincia: '', localidad: '', nombre: '', fecha: '', hora: '', cabezas: '', youtube: '' },
8208    feriaTitleAuto: true, feriaFavoriteId: '', feriaManualOpen:false,
8209    feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '',
8210    geoProvinces: [...ARGENTINA_PROVINCES], geoLocs: {}, geoLoadingProv: false, geoLoadingLoc: false,
8211    feriaPhase: 'idle', feriaResult: null, feriaError: '',
8212    selectedCons: null, consRemates: null, consRematesLoading: false, channelTab: 'todos', channelYear: null, chHeroSlide: 0, notiSlide: 0, mercIndex: 'piri', mercTipo: 'Invernada', mercHover: null, mercRange: 12,
8213    mavDate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 10), mavSummary: null, mavRanges: null, mavLoading: false, mavError: '', mavMetric: 'tasa_prom_pond', mavHover: null,
8214  };
8215
8216  emptyRosganLotForm() {
8217    return {
8218      remateId: '', consignatariaId: '', certifiedBy: null, comitenteId: '', establishmentId: '', paymentTermDays: [], paymentTermReference: '', otherPaymentTermDay: '', otherPaymentTermReference: '', formType: '', categoryId: '', breedIds: [],
8219      quantity: '', maleQuantity: '', femaleQuantity: '', motherQuantity: '', calfQuantity: '',
8220      isTermino: false, isAptoChina: false, description: '', descriptionOptionValues: [], ageValue: '', ageUnit: 'meses', dentition: '',
8221      reproductiveStatus: '', destination: '', bodyCondition: '', animalClass: '', hasMioMio: null,
8222      tickStatus: '', health: '', nutrition: '', daysOfConfinement: '', averageWeight: '', maxWeight: '', minWeight: '', weighing: '',
8223      desbaste: '', hasLiquidBrand: false, isAptoVisec: false, service: '', pregnancy: '', calving: '',
8224      subCategory: '', conditionType: '', deliveryType: '', finishing: '', estimatedYield: '',
8225    };
8226  }
8227  isTextEntryControl(element) {
8228    if (!element || element.nodeType !== 1) return false;
8229    const tag = String(element.tagName || '').toLowerCase();
8230    if (tag === 'textarea' || element.isContentEditable) return true;
8231    if (tag !== 'input') return false;
8232    const type = String(element.type || 'text').toLowerCase();
8233    return !['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'].includes(type);
8234  }
8235  captureTextEntryFocus() {
8236    if (this._formCompositionActive) return null;
8237    const element = document.activeElement;
8238    if (!this.isTextEntryControl(element)) return null;
8239    const templateId = element.getAttribute('data-dc-tpl') || '';
8240    const matching = templateId ?Array.from(document.querySelectorAll('[data-dc-tpl="' + templateId + '"]')).filter((item) => this.isTextEntryControl(item)) : [];
8241    let selectionStart = null; let selectionEnd = null; let selectionDirection = 'none';
8242    try {
8243      selectionStart = element.selectionStart;
8244      selectionEnd = element.selectionEnd;
8245      selectionDirection = element.selectionDirection || 'none';
8246    } catch {}
8247    return {
8248      element,
8249      route: this.state.route,
8250      templateId,
8251      templateIndex: matching.indexOf(element),
8252      id: element.id || '',
8253      name: element.getAttribute('name') || '',
8254      type: String(element.type || '').toLowerCase(),
8255      placeholder: element.getAttribute('placeholder') || '',
8256      selectionStart,
8257      selectionEnd,
8258      selectionDirection,
8259      scrollLeft: element.scrollLeft || 0,
8260      scrollTop: element.scrollTop || 0,
8261    };
8262  }
8263  restoreTextEntryFocus(snapshot) {
8264    if (!snapshot || snapshot.route !== this.state.route || this._formCompositionActive) return;
8265    const active = document.activeElement;
8266    if (active && active !== document.body && active !== snapshot.element && this.isTextEntryControl(active)) return;
8267    let target = snapshot.element?.isConnected ?snapshot.element : null;
8268    if (!target && snapshot.id) target = document.getElementById(snapshot.id);
8269    if (!target && snapshot.templateId) {
8270      const matching = Array.from(document.querySelectorAll('[data-dc-tpl="' + snapshot.templateId + '"]')).filter((item) => this.isTextEntryControl(item));
8271      target = matching[Math.max(0, snapshot.templateIndex)] || null;
8272    }
8273    if (!target && snapshot.name) target = Array.from(document.querySelectorAll('input,textarea')).find((item) => item.getAttribute('name') === snapshot.name && String(item.type || '').toLowerCase() === snapshot.type) || null;
8274    if (!target && snapshot.placeholder) target = Array.from(document.querySelectorAll('input,textarea')).find((item) => item.getAttribute('placeholder') === snapshot.placeholder && String(item.type || '').toLowerCase() === snapshot.type) || null;
8275    if (!this.isTextEntryControl(target) || target.disabled) return;
8276    if (document.activeElement !== target) {
8277      try { target.focus({ preventScroll: true }); } catch { try { target.focus(); } catch {} }
8278    }
8279    if (snapshot.selectionStart != null && snapshot.selectionEnd != null && typeof target.setSelectionRange === 'function') {
8280      try { target.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd, snapshot.selectionDirection); } catch {}
8281    }
8282    target.scrollLeft = snapshot.scrollLeft;
8283    target.scrollTop = snapshot.scrollTop;
8284  }
8285  scheduleTextEntryRender(delay = 120) {
8286    if (this._textEntryRenderTimer) clearTimeout(this._textEntryRenderTimer);
8287    this._textEntryRenderTimer = setTimeout(() => {
8288      if (this._formCompositionActive) { this.scheduleTextEntryRender(delay); return; }
8289      this._textEntryRenderTimer = null;
8290      this.forceUpdate();
8291      this.scheduleDomBindings();
8292    }, delay);
8293  }
8294  setState(update, callback) {
8295    window.rosganAudienceApp=this;
8296    if (this._handlingTextInput && this.isTextEntryControl(document.activeElement)) {
8297      const patch = typeof update === 'function' ?update(this.state) : update;
8298      this.state = { ...this.state, ...(patch || {}) };
8299      if (typeof callback === 'function') callback();
8300      this.scheduleTextEntryRender();
8301      return;
8302    }
8303    const snapshot = this.captureTextEntryFocus();
8304    super.setState(update, () => {
8305      if (typeof callback === 'function') callback();
8306      this.restoreTextEntryFocus(snapshot);
8307      window.RosganAudience?.sync(this);
8308      window.RosganNameFit?.sync();
8309      document.documentElement.toggleAttribute('data-rosgan-video-hidden',false);
8310      if(this.state.route==='rosganTvLower'&&this.state.rosganOpsConfig?.videoVisible===false)document.documentElement.setAttribute('data-rosgan-video-hidden','true');
8311    });
8312  }
8313  forceUpdate(callback) {
8314    const snapshot = this.captureTextEntryFocus();
8315    if (!this.__host) return;
8316    this.__host.forceUpdate(() => {
8317      if (typeof callback === 'function') callback();
8318      this.restoreTextEntryFocus(snapshot);
8319    });
8320  }
8321  rosganForm() { return this.state.rosganLotForm || this.emptyRosganLotForm(
8321); }
8322  bufferInputState(key, patch, delay = 90) {
8323    this.state = { ...this.state, ...(typeof patch === 'function' ?patch(this.state) : patch) };
8324    this._inputRenderTimers = this._inputRenderTimers || {};
8325    if (this._inputRenderTimers[key]) clearTimeout(this._inputRenderTimers[key]);
8326    this._inputRenderTimers[key] = setTimeout(() => {
8327      delete this._inputRenderTimers[key];
8328      this.forceUpdate();
8329      this.scheduleDomBindings();
8330    }, delay);
8331  }
8332  setRosganLotField(key, value, buffered = false) {
8333    const patch = { rosganLotForm: { ...this.rosganForm(), [key]: value }, rosganLotMessage: null };
8334    if (buffered) this.bufferInputState('rosgan-lot-' + key, patch);
8335    else this.setState(patch);
8336  }
8337  setRosganDescription(event) {
8338    const value = String(event.target?.value ?? event.currentTarget?.value ?? '');
8339    // Save immediately, without re-rendering the textarea while typing.
8340    this.state = { ...this.state, rosganLotForm: { ...this.rosganForm(), description: value }, rosganLotMessage: null };
8341    if (event.type === 'blur') this.forceUpdate();
8342  }
8343  selectRosganLotType(value) {
8344    const formType = String(value || '').toLowerCase();
8345    if (!['invernada', 'faena', 'vientres'].includes(formType)) return;
8346    const current = this.rosganForm();
8347    const selectedCategory = this.rosganSelectedCategory();
8348    const keepsCategory = String(selectedCategory?.formType || '').toLowerCase() === formType;
8349    this.setState({
8350      rosganLotForm: {
8351        ...current,
8352        formType,
8353        categoryId: keepsCategory ?current.categoryId : '',
8354        quantity: keepsCategory ?current.quantity : '',
8355        maleQuantity: keepsCategory ?current.maleQuantity : '',
8356        femaleQuantity: keepsCategory ?current.femaleQuantity : '',
8357        motherQuantity: keepsCategory ?current.motherQuantity : '',
8358        calfQuantity: keepsCategory ?current.calfQuantity : '',
8359      },
8360      rosganLotMessage: null,
8361    });
8362  }
8363  selectRosganLotCategory(value) {
8364    const categoryId = String(value || '');
8365    const category = (this.state.rosganLotCategories || []).find((item) => String(item.id) === categoryId);
8366    this.setState({
8367      rosganLotForm: { ...this.rosganForm(), categoryId, formType: String(category?.formType || this.rosganForm().formType || '').toLowerCase() },
8368      rosganLotMessage: null,
8369    });
8370  }
8371  toggleRosganLotDescription(value) {
8372    const option = this.rosganDescriptionChoices().find(o => [o.value,o.label].some(v => this.consKey(v) === this.consKey(value)));
8373    if (!option) return;
8374    const selected = this.rosganForm().descriptionOptionValues;
8375    const values = Array.isArray(selected) ? selected : [];
8376    const matches = item => [option.value,option.label].some(v => this.consKey(v) === this.consKey(item));
8377    this.setRosganLotField('descriptionOptionValues', values.some(matches) ? values.filter(item => !matches(item)) : [...values,option.value]);
8378  }
8379  toggleRosganBreed(id) {
8380    const selected = this.rosganForm().breedIds || [];
8381    const exists = selected.includes(id);
8382    const families = this.rosganBreedFamilies().filter((group) => group.key !== '__crosses__');
8383    const selectedFamilies = families.filter((group) => group.items.some((item) => selected.includes(String(item.id))));
8384    const targetFamily = families.find((group) => group.items.some((item) => String(item.id) === String(id)));
8385    const targetAlreadySelected = targetFamily && targetFamily.items.some((item) => selected.includes(String(item.id)));
8386    if (!exists && !targetAlreadySelected && selectedFamilies.length >= 3) {
8387      this.setState({ rosganLotMessage: { ok: false, text: 'Podés seleccionar hasta 3 razas.' } });
8388      return;
8389    }
8390    this.setRosganLotField('breedIds', exists ?selected.filter((item) => item !== id) : [...selected, id]);
8391  }
8392  rosganBreedIconMap() {
8393    return {
8394      angus: 'assets/breeds/angus.png', hereford: 'assets/breeds/hereford.png', brahman: 'assets/breeds/brahman.png',
8395      braford: 'assets/breeds/braford.png', brangus: 'assets/breeds/brangus.png', charolais: 'assets/breeds/charolais.png',
8396      holandoargentino: 'assets/breeds/holando-argentino.png', jersey: 'assets/breeds/jersey.png', santagertrudis: 'assets/breeds/santa-gertrudis.png',
8397      bonsmara: 'assets/breeds/bonsmara.png', bovinocriollo: 'assets/breeds/bovino-criollo.png', criollos: 'assets/breeds/bovino-criollo.png',
8398      limousin: 'assets/breeds/limousin.png', pardosuizo: 'assets/breeds/pardo-suizo.png', piemontese: 'assets/breeds/piemontese.png',
8399      retinta: 'assets/breeds/retinta.png', murraygrey: 'assets/breeds/murray-grey.png', shorthorn: 'assets/breeds/shorthorn.png', tuli: 'assets/breeds/tuli.png',
8400      polledhereford: 'assets/breeds/hereford.png', brafordcuartinas: 'assets/breeds/braford.png',
8401      murrah: 'assets/breeds/murrah.png', bubilla: 'assets/breeds/bubilla.png', hampshiredown: 'assets/breeds/hampshire-down.png',
8402      kiwicross: 'assets/breeds/kiwicross.png', anguscruzajersey: 'assets/breeds/angus-cruza-jersey.png', limangus: 'assets/breeds/limangus.png',
8403      senangus: 'assets/breeds/senangus.png', seneford: 'assets/breeds/seneford.png', sanignacio: 'assets/breeds/san-ignacio.png',
8404      caretas: 'assets/breeds/caretas.png', cruzas: 'assets/breeds/cruzas.png', cruzasbritanicas: 'assets/breeds/cruzas-britanicas.png',
8405      cruzasindicas: 'assets/breeds/cruzas-indicas.png', cuartinas: 'assets/breeds/cuartinas.png', cuartinos: 'assets/breeds/cuartinos.png',
8406      mestizas: 'assets/breeds/mestizas.png', mestizos: 'assets/breeds/mestizas.png', mestizosas: 'assets/breeds/mestizas.png',
8407      pampas: 'assets/breeds/pampas.png', pampizada: 'assets/breeds/pampizada.png', negrosy: 'assets/breeds/cruzas.png',
8408    };
8409  }
8410  rosganBreedIcon(name) {
8411    return this.rosganBreedIcons(name)[0]?.icon || '';
8412  }
8413  rosganBreedIcons(name) {
8414    const source = String(name || '').trim();
8415    if (!source) return [];
8416    const icons = this.rosganBreedIconMap();
8417    const aliases = [...Object.keys(icons).filter((item) => item !== 'negrosy'), 'careta']
8418      .sort((a, b) => b.length - a.length);
8419    const found = [];
8420    source.split(/[.,;|/+]+/).map((part) =>
8420 part.trim()).filter(Boolean).forEach((part) => {
8421      let remaining = this.consKey(part).split('ysuscruzas').join('');
8422      if (!remaining) return;
8423      const showBlack = /negr/i.test(part);
8424      const showRed = /colorad/i.test(part);
8425      aliases.forEach((alias) => {
8426        if (!remaining.includes(alias)) return;
8427        const mapKey = alias === 'careta' ?'caretas' : alias;
8428        const icon = icons[mapKey];
8429        if (icon) {
8430          const existing = found.find((item) => item.icon === icon);
8431          if (existing) { existing.showBlack = existing.showBlack || showBlack; existing.showRed = existing.showRed || showRed; }
8432          else found.push({ icon, showBlack, showRed });
8433        }
8434        remaining = remaining.split(alias).join('');
8435      });
8436    });
8437    return found;
8438  }
8439  rosganBreedFamilies() {
8440    const groups = new Map();
8441    const family = (key, label) => {
8442      if (!groups.has(key)) groups.set(key, { key, label, base: null, black: null, red: null, both: null, items: [] });
8443      return groups.get(key);
8444    };
8445    (this.state.rosganLotBreeds || []).forEach((item) => {
8446      const name = String(item?.name || item?.nombre || '').trim();
8447      if (!name) return;
8448      const id = String(item.id);
8449      if (/^y\s+sus\s+cruzas$/i.test(name)) {
8450        const group = family('__crosses__', 'Y sus cruzas');
8451        group.base = { ...item, id, name: 'Y sus cruzas' };
8452        group.items.push(group.base);
8453        return;
8454      }
8455      const bothMatch = name.match(/^(.*?)\s+(negros?|negras?)\s+y\s+(colorados?|coloradas?)$/i);
8456      const singleMatch = name.match(/^(.*?)\s+(negros?|negras?|colorados?|coloradas?)$/i);
8457      if (bothMatch?.[1]?.trim()) {
8458        const label = bothMatch[1].trim();
8459        const group = family(this.consKey(label), label);
8460        group.both = { ...item, id, name };
8461        group.items.push(group.both);
8462        return;
8463      }
8464      if (singleMatch?.[1]?.trim()) {
8465        const label = singleMatch[1].trim();
8466        const group = family(this.consKey(label), label);
8467        const variant = { ...item, id, name };
8468        if (/negr/i.test(singleMatch[2])) group.black = variant;
8469        else group.red = variant;
8470        group.items.push(variant);
8471        return;
8472      }
8473      const group = family(this.consKey(name), name);
8474      const base = { ...item, id, name };
8475      group.base = base;
8476      group.items.push(base);
8477    });
8478    return Array.from(groups.values()).map((group) => {
8479      const hasIndividualColors = !!(group.black || group.red);
8480      if (!hasIndividualColors && group.both && !group.base) {
8481        return { ...group, key: group.key + '__both', label: group.both.name, base: group.both, both: null, items: [group.both], supportsColors: false };
8482      }
8483      return { ...group, supportsColors: hasIndividualColors };
8484    }).sort((a, b) => {
8485      if (a.key === '__crosses__') return -1;
8486      if (b.key === '__crosses__') return 1;
8487      return a.label.localeCompare(b.label, 'es');
8488    });
8489  }
8490  selectRosganBreedFamily(key) {
8491    const group = this.rosganBreedFamilies().find((item) => item.key === key);
8492    if (!group) return;
8493    const selected = this.rosganForm().breedIds || [];
8494    const groupIds = group.items.map((item) => String(item.id));
8495    const isSelected = groupIds.some((id) => selected.includes(id));
8496    if (!group.supportsColors) {
8497      const id = String(group.base?.id || group.items[0]?.id || '');
8498      if (id) this.toggleRosganBreed(id);
8499      return;
8500    }
8501    if (!isSelected) {
8502      const id = String(group.base?.id || group.both?.id || group.black?.id || group.red?.id || '');
8503      if (!id) return;
8504      const selectedFamilies = this.rosganBreedFamilies().filter((candidate) => candidate.key !== '__crosses__' && candidate.items.some((item) => selected.includes(String(item.id))));
8505      if (selectedFamilies.length >= 3) { this.setState({ rosganLotMessage: { ok: false, text: 'Podés seleccionar hasta 3 razas.' } }); return; }
8506      this.setState({ rosganLotForm: { ...this.rosganForm(), breedIds: [...selected, id] }, rosganActiveBreedFamily: key, rosganLotMessage: null });
8507      return;
8508    }
8509    this.setState({ rosganActiveBreedFamily: key, rosganLotMessage: null });
8510  }
8511  removeRosganBreedFamily(key) {
8512    const group = this.rosganBreedFamilies().find((item) => item.key === key);
8513    if (!group) return;
8514    const ids = new Set(group.items.map((item) => String(item.id)));
8515    const crossesGroup = this.rosganBreedFamilies().find((item) => item.key === '__crosses__');
8516    const crossesId = String(crossesGroup?.base?.id || crossesGroup?.items?.[0]?.id || '');
8517    const nextCrosses = { ...(this.state.rosganBreedCrosses || {}) };
8518    delete nextCrosses[key];
8519    let nextBreedIds = (this.rosganForm().breedIds || []).filter((id) => !ids.has(String(id)) && (!crossesId || String(id) !== crossesId));
8520    if (crossesId && Object.values(nextCrosses).some(Boolean)) nextBreedIds.push(crossesId);
8521    this.setState({
8522      rosganLotForm: { ...this.rosganForm(), breedIds: nextBreedIds },
8523      rosganBreedCrosses: nextCrosses,
8524      rosganActiveBreedFamily: this.state.rosganActiveBreedFamily === key ?'' : this.state.rosganActiveBreedFamily,
8525      rosganLotMessage: null,
8526    });
8527  }
8528  toggleRosganBreedColor(key, color) {
8529    const group = this.rosganBreedFamilies().find((item) => item.key === key);
8530    if (!group?.supportsColors) return;
8531    const selected = this.rosganForm().breedIds || [];
8532    const groupIds = new Set(group.items.map((item) => String(item.id)));
8533    const outside = selected.filter((id) => !groupIds.has(String(id)));
8534    let black = !!group.both && selected.includes(String(group.both.id)) || !!group.black && selected.includes(String(group.black.id));
8535    let red = !!group.both && selected.includes(String(group.both.id)) || !!group.red && selected.includes(String(group.red.id));
8536    if (color === 'black') black = !black;
8537    if (color === 'red') red = !red;
8538    const next = [];
8539    if (black && red && group.both) next.push(String(group.both.id));
8540    else {
8541      if (black && group.black) next.push(String(group.black.id));
8542      if (red && group.red) next.push(String(group.red.id));
8543    }
8544    if (!next.length && group.base) next.push(String(group.base.id));
8545    this.setState({ rosganLotForm: { ...this.rosganForm(), breedIds: [...outside, ...next] }, rosganActiveBreedFamily: key, rosganLotMessage: null });
8546  }
8547  toggleRosganBreedCrosses(key) {
8548    const group = this.rosganBreedFamilies().find((item) => item.key === key);
8549    const crossesGroup = this.rosganBreedFamilies().find((item) => item.key === '__crosses__');
8550    const crossesId = String(crossesGroup?.base?.id || crossesGroup?.items?.[0]?.id || '');
8551    if (!group || !crossesId) return;
8552    const nextCrosses = { ...(this.state.rosganBreedCrosses || {}), [key]: !this.state.rosganBreedCrosses?.[key] };
8553    if (!nextCrosses[key]) delete nextCrosses[key];
8554    const withoutCrosses = (this.rosganForm().breedIds || []).filter((id) => String(id) !== crossesId);
8555    const nextBreedIds = Object.values(nextCrosses).some(Boolean) ?[...withoutCrosses, crossesId] : withoutCrosses;
8556    this.setState({ rosganLotForm: { ...this.rosganForm(), breedIds: nextBreedIds }, rosganBreedCrosses: nextCrosses, rosganLotMessage: null });
8557  }
8558  rosganArray(data) {
8559    if (Array.isArray(data)) return data;
8560    for (const key of ['data', 'items', 'results', 'remates', 'lots', 'videos', 'categories', 'breeds', 'consignatarias', 'comitentes', 'establishments']) {
8561      if (Array.isArray(data?.[key])) return data[key];
8562    }
8563    if (data?.data && typeof data.data === 'object') return this.rosganArray(data.data);
8564    return [];
8565  }
8566  async loadRosganLotOptions(force = false) {
8567    if (this.state.rosganLotOptionsLoading || (!force && this.state.rosganLotOptions)) return;
8568    this.setState({ rosganLotOptionsLoading: true, rosganLotOptionsError: '' });
8569    try {
8570      const response = await this.apiFetch('/api/rosgan-lots/options');
8571      const options = response?.data && !Array.isArray(response.data) ?response.data : response;
8572      this.setState({ rosganLotOptions: options || {}, rosganLotOptionsLoading: false, rosganLotOptionsError: '' });
8573    } catch (error) {
8574      this.setState({ rosganLotOptionsLoading: false, rosganLotOptionsError: error?.message || 'No se pudieron cargar las opciones.' });
8575    }
8576  }
8577  rosganFieldOptions(...aliases) {
8578    const source = this.state.rosganLotOptions;
8579    if (!source) return [];
8580    const normalize = (value) => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
8581    const targets = new Set(aliases.flatMap((alias) => [alias, alias + 's', alias + 'Options', alias + 'Values', alias + 'List']).map(normalize));
8582    const containers = [source];
8583    const visited = new Set();
8584    let raw = null;
8585    while (containers.length && !raw) {
8586      const container = containers.shift();
8587      if (!container || typeof container !== 'object' || visited.has(container)) continue;
8588      visited.add(container);
8589      if (Array.isArray(container)) {
8590        const field = container.find((item) => targets.has(normalize(item?.key || item?.field || item?.name || item?.
8590id)));
8591        if (field) { raw = field.options || field.values || field.items || field.data; break; }
8592        container.filter((item) => item && typeof item === 'object').forEach((item) => containers.push(item));
8593        continue;
8594      }
8595      const key = Object.keys(container || {}).find((candidate) => targets.has(normalize(candidate)));
8596      if (key) { raw = container[key]; break; }
8597      Object.values(container).filter((value) => value && typeof value === 'object').forEach((value) => containers.push(value));
8598    }
8599    if (!Array.isArray(raw)) raw = raw?.options || raw?.values || raw?.items || raw?.data || [];
8600    const seen = new Set();
8601    return (Array.isArray(raw) ?raw : []).map((item) => {
8602      const value = item && typeof item === 'object' ?(item.value ?? item.id ?? item.code ?? item.name ?? item.label) : item;
8603      const label = item && typeof item === 'object' ?(item.label ?? item.displayName ?? item.name ?? item.description ?? value) : item;
8604      return { value: String(value ?? ''), label: String(label ?? value ?? '') };
8605    }).filter((item) => item.value && !seen.has(item.value) && seen.add(item.value));
8606  }
8607  rosganAssignedConsignatarias(consignatarias = this.state.rosganLotConsignatarias || [], relations = null) {
8608    if (this.canManageUsers(this.state.apiUser)) return null;
8609    const memberships = Array.isArray(relations) ?relations : [...(this.state.userConsignatarias || []), this.state.userConsignataria];
8610    const refs = memberships
8611      .flatMap((item) => [item?.id, item?.slug, item?.ref, item?.name]).filter(Boolean);
8612    return consignatarias.filter((item) => refs.some((ref) => [item?.id, item?.slug, item?.name, item?.nombre]
8613      .some((value) => value && this.consKey(value) === this.consKey(ref))));
8614  }
8615  rosganAssignedConsignataria(consignatarias = this.state.rosganLotConsignatarias || [], relations = null) {
8616    return this.rosganAssignedConsignatarias(consignatarias, relations)?.[0] || null;
8617  }
8618  rosganEligibleRemates(remates) {
8619    return (remates || []).filter((item) => item?.activo !== false && item?.cargaDeLotes !== false && item?.closeRemate !== true && !item?.cabanasConfig);
8620  }
8621  rosganUpcomingRemates(remates) {
8622    return (remates || []).filter((item) => item?.activo !== false && item?.closeRemate !== true && !item?.cabanasConfig);
8623  }
8624  homeFeaturedRemates(historical, current) {
8625    const active = this.rosganUpcomingRemates(current)
8626      .map((item) => this.normalizeRosganActiveRemate(item));
8627    const seen = new Set();
8628    return [...active, ...(historical || [])]
8629      .filter((item) => !this.isExcludedRemate(item) && this.homeAgendaIncludes(this.remateDate(item)))
8630      .filter((item) => {
8631        const key = item.__feriaAuction ?'feria:' + item.feriaId : this.rosganArchiveIdentity(item);
8632        if (seen.has(key)) return false;
8633        seen.add(key);
8634        return true;
8635      })
8636      .sort((a, b) => this.remateDate(a) - this.remateDate(b));
8637  }
8638  rosganArchiveIdentity(item) {
8639    const title = item?.nombre_remate || item?.titulo || '';
8640    const year = this.remateDate(item)?.getFullYear() || '';
8641    return `${this.consKey(title)}:${year}`;
8642  }
8643  rosganScheduledRemates(items, current) {
8644    const existing = new Set((current || []).map((item) => this.rosganArchiveIdentity(item)));
8645    const inactive = (current || []).filter((item) => (item.activo === false || item.cargaDeLotes === false) && !item.closeRemate && !item.cabanasConfig)
8646      .map((item) => ({ ...this.normalizeRosganActiveRemate(item), __rosganScheduled: true }));
8647    return [...inactive, ...(items || [])].filter((item) => {
8648      const typeAndName = this.consKey([item.nombre_tipo, item.tipo_remate, item.nombre_remate].filter(Boolean).join(' '));
8649      return !this.isExcludedRemate(item) && !typeAndName.includes('cabana') && this.remateType(item)?.[0] !== 'Cabañas'
8650        && (item.__rosganScheduled || this.homeAgendaIncludes(this.remateDate(item))) && (item.__rosganScheduled || !existing.has(this.rosganArchiveIdentity(item)));
8651    }).sort((a, b) => this.remateDate(a) - this.remateDate(b));
8652  }
8653  rosganArchiveRemates(historical, current, year) {
8654    const selectedYear = Number(year);
8655    const closed = (current || [])
8656      .filter((item) => item?.closeRemate === true && !item?.cabanasConfig && !this.isExcludedRemate(item))
8657      .map((item) => ({ ...this.normalizeRosganActiveRemate(item), __rosganClosedArchive: true }))
8658      .filter((item) => !selectedYear || this.remateDate(item)?.getFullYear() === selectedYear);
8659    const replacements = new Set(closed.map((item) => this.rosganArchiveIdentity(item)));
8660    const legacy = (historical || [])
8661      .filter((item) => this.isRosganHistoricalRemate(item))
8662      .filter((item) => !replacements.has(this.rosganArchiveIdentity(item)));
8663    return [...closed, ...legacy];
8664  }
8665  emptyRosganRemateForm(item = null) {
8666    const sourceDate = item?.fechaHoraRemate ?new Date(item.fechaHoraRemate) : null;
8667    const validDate = sourceDate && !Number.isNaN(sourceDate.getTime());
8668    const pad = (value) => String(value).padStart(2, '0');
8669    const date = validDate ?`${sourceDate.getFullYear()}-${pad(sourceDate.getMonth() + 1)}-${pad(sourceDate.getDate())}` : '';
8670    const time = validDate ?`${pad(sourceDate.getHours())}:${pad(sourceDate.getMinutes())}` : '';
8671    return {
8672      titulo: item?.titulo || '', fecha: date, hora: time, cabezas: item?.cabezas ?String(item.cabezas) : '',
8673      tipoRemate: item?.tipoRemate || String(this.state.rosganRemateTypes?.[0]?.id || ''), modoConsignataria: item?.modoConsignataria || 'Individual',
8674      consignatariaId: String(item?.consignatariaId || ''), lugar: item?.lugar || '', provincia: item?.provincia || '', localidad: item?.localidad || '',
8675      canalRural: !!item?.canalRural,
8676      cargaDeLotes: item?.cargaDeLotes ?? false, closeRemate: !!item?.closeRemate,
8677      informaIndice: item?.informaIndice ?? false, mostrarComitentes: item?.mostrarComitentes ?? false,
8678    };
8679  }
8680  normalizeRosganRemates(data) {
8681    if (Array.isArray(data)) return data;
8682    const root = data?.data ?? data;
8683    if (Array.isArray(root)) return root;
8684    for (const key of ['items', 'remates', 'results']) if (Array.isArray(root?.[key])) return root[key];
8685    return root?.id ?[root] : [];
8686  }
8687  remateManualShareHeads(remate) {
8688    const heads = Number(remate?.cabezas);
8689    return Number.isSafeInteger(heads) && heads > 0 ? heads : 0;
8690  }
8691  rosganRemateHeads(remate, lots = []) {
8692    const manual = Number(remate?.cabezas);
8693    if (Number.isFinite(manual) && manual > 0) return manual;
8694    return lots.reduce((sum, lot) => {
8695      const source = { ...(lot?.data || lot?.lot || {}), ...lot };
8696      const count = Number(source.totalQuantity ?? source.quantity ?? source.cantidad);
8697      const split = (Number(source.maleQuantity ?? source.cantidadMachos) || 0) + (Number(source.femaleQuantity ?? source.cantidadHembras) || 0)
8698        || (Number(source.motherQuantity ?? source.cantidadMadres) || 0) + (Number(source.calfQuantity ?? source.cantidadCrias) || 0);
8699      return sum + (Number.isFinite(count) && count > 0 ? count : Math.max(0, split));
8700    }, 0);
8701  }
8702  async loadRosganAutomaticHeads(remates) {
8703    return Promise.all(remates.map(async (item) => {
8704      if (Number(item.cabezas) > 0) return item;
8705      try {
8706        const lots = this.rosganArray(await this.apiFetch('/api/remates/' + encodeURIComponent(item.id) + '/rosgan-lots'));
8707        return { ...item, __automaticHeads: this.rosganRemateHeads(item, lots) };
8708      } catch { return { ...item, __automaticHeads: null }; }
8709    }));
8710  }
8711  normalizeRosganRemateTypes(data) {
8712    const root = data?.data ?? data;
8713    const items = Array.isArray(root) ?root : (root?.items || root?.tiposRemate || root?.results || []);
8714    return (Array.isArray(items) ?items : [])
8715      .filter((item) => item?.isActive === true)
8716      .map((item) => ({
8717        ...item,
8718        id: String(item?.id || '').trim(),
8719        name: String(item?.name || item?.nombre || item?.id || '').trim(),
8720        permiteModoTodos: item?.permiteModoTodos === true,
8721      }))
8722      .filter((item) => item.id && item.name)
8723      .sort((a, b) => Number(a.order ?? Number.MAX_SAFE_INTEGER) - Number(b.order ?? Number.MAX_SAFE_INTEGER) || a.name.localeCompare(b.name, 'es'));
8724  }
8725  rosganRemateType(value, types = this.state.rosganRemateTypes || []) {
8726    const key = this.consKey(value);
8727    return types.find((item) => this.consKey(item.id) === key || this.consKey(item.name) === key) || null;
8728  }
8729  async loadRosganRemateTypes(force = false) {
8730    if (this.state.rosganRemateTypesLoading || (!force && this.state.rosganRemateTypes)) return;
8731    this.setState({ rosganRemateTypesLoading: true, rosganRemateTypesError: '' });
8732    try {
8733      const types = this.normalizeRosganRemateTypes(await this.apiFetch('/api/tipos-remate'));
8734      if (!types.length) throw new Error('No hay tipos de remate activos disponibles.');
8735      const form = this.state.rosganRemateForm;
8736      const selected = this.rosganRemateType(form?.tipoRemate, types) || types[0];
8737      const nextForm = form ?{
8738        ...form,
8739        tipoRemate: selected.id,
8740        modoConsignataria: selected.permiteModoTodos && form.modoConsignataria === 'Todos' ?'Todos' : 'Individual',
8741        consignatariaId: selected.permiteModoTodos && form.modoConsignataria === 'Todos' ?'' : form.consignatariaId,
8742      } : form;
8743      this.setState({ rosganRemateTypes: types, rosganRemateTypesLoading: false, rosganRemateTypesError: '', rosganRemateForm: nextForm });
8744    } catch (error) {
8745      this.setState({ rosganRemateTypes: [], rosganRemateTypesLoading: false, rosganRemateTypesError: error?.message || 'No se pudieron cargar los tipos de remate.' });
8746    }
8747  }
8748  async loadRosganRemateAdmin(force = false) {
8749    if (this._rosganRematesRequest) return this._rosganRematesRequest;
8750    if (!force && this.state.rosganRemates && this.state.rosganLotConsignatarias) return this.state.rosganRemates;
8751    const uid = String(this.state.user?.uid || this.state.apiUser?.uid || this.state.apiUser?.userId || '');
8752    const cacheKey = 'rosgan:remates:v3:public';
8753    const cached = !Array.isArray(this.state.rosganRemates) && cacheKey ?this.readCollectionCache(cacheKey) : null;
8754    const fallbackRemates = Array.isArray(cached?.remates) ?cached.remates : this.state.rosganRemates;
8755    const fallbackConsignatarias = Array.isArray(cached?.consignatarias) ?cached.consignatarias : this.state.rosganLotConsignatarias;
8756    const hadLiveConsignatarias = Array.isArray(this.state.rosganLotConsignatarias);
8757    this.setState({
8758      rosganRemates: Array.isArray(fallbackRemates) ?fallbackRemates : this.state.rosganRemates,
8759      rosganLotConsignatarias: Array.isArray(fallbackConsignatarias) ?fallbackConsignatarias : this.state.rosganLotConsignatarias,
8760      rosganRematesLoading: true,
8761      rosganRematesError: '',
8762    });
8763    const request = (async () => {
8764      try {
8765        const isAdmin = this.canManageUsers(this.state.apiUser);
8766        const canManage = this.canManageSellerData(this.state.apiUser);
8767        const knownRelations = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(Boolean);
8768        const shouldRefreshConsignatarias = force || !hadLiveConsignatarias;
8769        const consignatariasRequest = shouldRefreshConsignatarias
8770          ?this.apiFetchWithFallback('/api/rosgan-consignatarias', '/api/consignatarias').catch(() => [])
8771          : Promise.resolve(null);
8772        const relationsRequest = canManage && !isAdmin && !knownRelations.length && uid
8773          ?this.fetchUserConsignataria(uid)
8774          : Promise.resolve(knownRelations);
8775        const publicRematesRequest = this.apiFetch('/api/Remates');
8776        const [consignatariasRaw, resolvedRelations, publicRematesRaw] = await Promise.all([
8777          consignatariasRequest,
8778          relationsRequest,
8779          publicRematesRequest,
8780        ]);
8781        const consignatariasSource = consignatariasRaw
8782          ?this.rosganArray(consignatariasRaw)
8783          : (this.state.rosganLotConsignatarias || fallbackConsignatarias || []);
8784        const consignatarias = consignatariasSource.filter((item) => item?.isActive !== false && item?.activo !== false);
8785        const relations = Array.isArray(resolvedRelations) && resolvedRelations.length ?resolvedRelations : knownRelations;
8786        const assigned = canManage ?this.rosganAssignedConsignataria(consignatarias, relations) : null;
8787        const remates = await this.loadRosganAutomaticHeads(this.normalizeRosganRemates(publicRematesRaw).filter((item) => !item?.cabanasConfig));
8788        const currentForm = this.state.rosganRemateForm;
8789        const nextForm = currentForm && !currentForm.consignatariaId && assigned
8790          ?{ ...currentForm, consignatariaId: String(assigned.id) }
8791          : currentForm;
8792        this.writeCollectionCache(cacheKey, { remates, consignatarias }, 30 * 60 * 1000);
8793        this.setState({
8794          rosganRemates: remates, rosganLotConsignatarias: consignatarias, rosganRematesLoading: false,
8795          rosganRematesError: '', rosganRemateForm: nextForm,
8796        });
8797        this.loadRemateListCatalogs(remates);
8798        return remates;
8799      } catch (error) {
8800        this.setState({
8801          rosganRemates: Array.isArray(this.state.rosganRemates) ?this.state.rosganRemates : (fallbackRemates || []),
8802          rosganRematesLoading: false,
8803          rosganRematesError: error?.message || 'No se pudieron cargar los remates Rosgan.',
8804        });
8805        return null;
8806      }
8807    })();
8808    this._rosganRematesRequest = request;
8809    try { return await request; }
8810    finally { if (this._rosganRematesRequest === request) this._rosganRematesRequest = null; }
8811  }
8812  chooseRemateCreationType() {
8813    if (!this.canManageUsers(this.state.apiUser)) return;
8814    if (document.getElementById('remate-create-choice')) return;
8815    const previousFocus = document.activeElement;
8816    const dialog = document.createElement('dialog');
8817    dialog.id = 'remate-create-choice';
8818    dialog.setAttribute('aria-labelledby', 'remate-create-choice-title');
8819    dialog.style.cssText = 'width:min(520px,calc(100vw - 32px));box-sizing:border-box;padding:24px;border:1px solid #dce7f2;border-radius:20px;color:#112033;background:#fff;box-shadow:0 24px 80px #001d3b40;font-family:inherit;max-height:calc(100dvh - 32px);overflow:auto;';
8820    dialog.innerHTML = `
8821      <style>#remate-create-choice::backdrop{background:rgba(0,25,52,.5);backdrop-filter:blur(3px)}#remate-create-choice .remate-type-choice{display:flex;align-items:center;gap:14px;width:100%;padding:18px;margin-top:12px;text-align:left;font:inherit;color:#12304d;border:1px solid #dce7f2;border-radius:14px;background:#f7faff;cursor:pointer}#remate-create-choice .remate-type-choice:hover{background:#eaf3ff;border-color:#0a5ca8}#remate-create-choice button:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px}#remate-create-choice .remate-type-choice img{width:26px;height:26px;flex:none}#remate-create-choice strong{display:block;font-size:16px}#remate-create-choice small{display:block;margin-top:5px;font-size:13px;line-height:1.5;color:#61758c}</style>
8822      <div style="display:flex;align-items:center;justify-content:space-between;gap:12px"><h2 id="remate-create-choice-title" style="margin:0;font-size:23px">Crear remate</h2><button type="button" data-close aria-label="Cerrar" style="border:1px solid #dce7f2;border-radius:50%;width:36px;height:36px;background:white;cursor:pointer;font-size:23px;color:#12304d">×</button></div>
8823      <p style="margin:10px 0 20px;color:#61758c;font-size:14px">Elegí qué tipo de remate querés crear.</p>
8824      <button type="button" class="remate-type-choice" data-kind="rosgan"><img src="/assets/icons/lucide/clipboard-list.svg" alt=""><span><strong>Invernada / Cría / Faena</strong><small>Crear un remate Rosgan.</small></span><span aria-hidden="true" style="margin-left:auto">→</span></button>
8825      <button type="button" class="remate-type-choice" data-kind="cabana"><img src="/assets/icons/lucide/warehouse.svg" alt=""><span><strong>Cabaña</strong><small>Crear un remate de cabaña.</small></span><span aria-hidden="true" style="margin-left:auto">→</span></button>`;
8826    dialog.querySelector('[data-close]').onclick = () => dialog.close();
8827    dialog.addEventListener('click', event => {
8828      if (event.target === dialog) {
8829        const bounds = dialog.getBoundingClientRect();
8830        if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) dialog.close();
8831      }
8832    });
8833    dialog.addEventListener('close', () => {
8834      dialog.remove();
8835      if (previousFocus?.isConnected) previousFocus.focus();
8836    }, { once: true });
8837    dialog.querySelectorAll('[data-kind]').forEach(button => {
8838      button.onclick = () => {
8839        const kind = button.dataset.kind;
8840        dialog.close();
8841        if (kind === 'cabana') this.openRemateConfig('Remate Cabañas');
8842        else this.openRosganRemateForm();
8843      };
8844    });
8845    document.body.appendChild(dialog);
8846    dialog.showModal();
8847    dialog.querySelector('[data-kind]').focus();
8848  }
8849  openRosganRemateForm(item = null, { navigate = true } = {}) {
8850    if (!this.canManageUsers(this.state.apiUser)) { this.showToast('Solo administradores pueden crear o editar remates.'); return; }
8851    this.fetchProvinces();
8852    const assigned = this.rosganAssignedConsignataria(this.state.rosganLotConsignatarias || []);
8853    const form = this.emptyRosganRemateForm(item);
8854    if (!this.canManageUsers(this.state.apiUser) && assigned) form.consignatariaId = String(assigned.id);
8855    this.resetRosganRematePlaces();
8856    if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview);
8857    this.setState({
8858      rosganRemateFormOpen: true, rosganRemateEditingId: String(item?.id || ''), rosganRemateForm: form,
8859      rosganRemateMessage: null, rosganRemateFlyer: null, rosganRemateFlyerPreview: item?.flyerUrl || '',
8860    }, () => { if (navigate) this.go('rosganRemateCreate'); });
8861    this.loadRosganRemateTypes(true);
8862    if (!this.state.rosganLotConsignatarias) this.loadRosganRemateAdmin(true);
8863  }
8864  closeRosganRemateForm() {
8865    this.resetRosganRematePlaces();
8866    if (this.state.rosganRemateSaving) return;
8867    if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview);
8868    this.setState({ rosganRemateFormOpen: false, rosganRemateEditingId: '', rosganRemateForm: null, rosganRemateMessage: null, rosganRemateFlyer: null, rosganRemateFlyerPreview: '' }, () => this.go('remates'));
8869  }
8870  setRosganRemateField(key, value) {
8871    const form = { ...(this.state.rosganRemateForm || this.emptyRosganRemateForm()), [key]: value };
8872    if (key === 'provincia') { form.localidad = ''; this.resetRosganRematePlaces(); }
8873    if (key === 'tipoRemate') {
8874      const type = this.rosganRemateType(value);
8875      if (!type?.permiteModoTodos) form.modoConsignataria = 'Individual';
8876    }
8877    if (key === 'modoConsignataria' && value === 'Todos') form.consignatariaId = '';
8878    this.setState({ rosganRemateForm: form, rosganRemateMessage: null });
8879  }
8880  resetRosganRematePlaces() {
8881    clearTimeout(this._rrfPlaceTimer); clearTimeout(this._rrfPlaceBlur);
8882    this._rrfPlaceToken = (this._rrfPlaceToken || 0) + 1;
8883    this.setState({rrfPlaceResults:[],rrfPlaceLoading:false,rrfPlaceError:'',rrfPlaceOpen:false,rrfPlaceIndex:-1});
8884  }
8885  onRosganRematePlaceInput(value) {
8886    this.resetRosganRematePlaces();
8887    this.setRosganRemateField('localidad', value);
8888    const form = this.state.rosganRemateForm;
8889    if (!form?.provincia || String(value).trim().length < 3) return;
8890    const token = this._rrfPlaceToken;
8891    this._rrfPlaceTimer = setTimeout(() => this.searchRosganRematePlaces(value,form.provincia,token),350);
8892  }
8893  async searchRosganRematePlaces(value,province,token) {
8894    if (token !== this._rrfPlaceToken) return;
8895    this.setState({rrfPlaceLoading:true,rrfPlaceError:'',rrfPlaceOpen:true});
8896    try {
8897      const response = await this.apiFetch('/api/places/search',{method:'POST',body:JSON.stringify({query:String(value).trim(),provincia:province})});
8898      if (token !== this._rrfPlaceToken) return;
8899      const results = this.listFrom(response,['data','items','results']).slice(0,7);
8900      this.setState({rrfPlaceResults:results,rrfPlaceLoading:false,rrfPlaceError:results.length?'':'No encontramos esa localidad. Podés escribirla manualmente.'});
8901    } catch {
8902      if (token !== this._rrfPlaceToken) return;
8903      this.setState({rrfPlaceResults:[],rrfPlaceLoading:false,rrfPlaceError:'No se pudo consultar la ubicación. Podés escribirla manualmente.'});
8904    }
8905  }
8906  selectRosganRematePlace(item) {
8907    if (!item) return;
8908    const input = typeof document !== 'undefined' ?document.getElementById('rrf-locality') : null;
8909    input?.blur();
8910    this.resetRosganRematePlaces();
8911    this.setRosganRemateField('localidad',String(item.label || item.address || ''));
8912    if (input) input.value = String(item.label || item.address || '');
8913  }
8914  rosganRematePlaceKey(event) {
8915    const items=this.state.rrfPlaceResults || [];
8916    if(event.key==='Escape'){event.preventDefault();this.resetRosganRematePlaces();return;}
8917    if(!items.length)return;
8918    if(['ArrowDown','ArrowUp'].includes(event.key)){
8919      event.preventDefault();const direction=event.key==='ArrowDown'?1:-1;
8920      this.setState({rrfPlaceIndex:((this.state.rrfPlaceIndex ?? -1)+direction+items.length)%items.length,rrfPlaceOpen:true});
8921    } else if(event.key==='Enter' && this.state.rrfPlaceIndex>=0){event.preventDefault();this.selectRosganRematePlace(items[this.state.rrfPlaceIndex]);}
8922  }
8923  resetCabanaRematePlaces() {
8924    clearTimeout(this._rcPlaceTimer); clearTimeout(this._rcPlaceBlur);
8925    this._rcPlaceToken = (this._rcPlaceToken || 0) + 1;
8926    this.setState({rcPlaceResults:[],rcPlaceLoading:false,rcPlaceError:'',rcPlaceOpen:false,rcPlaceIndex:-1});
8927  }
8928  onCabanaRematePlaceInput(value) {
8929    this.resetCabanaRematePlaces();
8930    this.setRemateConfigField('localidad', value);
8931    const form = this.state.remateConfigForm;
8932    if (!form?.provincia || String(value).trim().length < 3) return;
8933    const token = this._rcPlaceToken;
8934    this._rcPlaceTimer = setTimeout(() => this.searchCabanaRematePlaces(value,form.provincia,token),350);
8935  }
8936  async searchCabanaRematePlaces(value,province,token) {
8937    if (token !== this._rcPlaceToken) return;
8938    this.setState({rcPlaceLoading:true,rcPlaceError:'',rcPlaceOpen:true});
8939    try {
8940      const response = await this.apiFetch('/api/places/search',{method:'POST',body:JSON.stringify({query:String(value).trim(),provincia:province})});
8941      if (token !== this._rcPlaceToken) return;
8942      const results = this.listFrom(response,['data','items','results']).slice(0,7);
8943      this.setState({rcPlaceResults:results,rcPlaceLoading:false,rcPlaceError:results.length?'':'No encontramos esa localidad. Podés escribirla manualmente.'});
8944    } catch {
8945      if (token !== this._rcPlaceToken) return;
8946      this.setState({rcPlaceResults:[],rcPlaceLoading:false,rcPlaceError:'No se pudo consultar la ubicación. Podés escribirla manualmente.'});
8947    }
8948  }
8949  selectCabanaRematePlace(item) {
8950    if (!item) return;
8951    const input = typeof document !== 'undefined' ?document.getElementById('rc-locality') : null;
8952    input?.blur();
8953    this.resetCabanaRematePlaces();
8954    this.setRemateConfigField('localidad',String(item.label || item.address || ''));
8955    if (input) input.value = String(item.label || item.address || '');
8956  }
8957  cabanaRematePlaceKey(event) {
8958    const items=this.state.rcPlaceResults || [];
8959    if(event.key==='Escape'){event.preventDefault();this.resetCabanaRematePlaces();return;}
8960    if(!items.length)return;
8961    if(['ArrowDown','ArrowUp'].includes(event.key)){
8962      event.preventDefault();const direction=event.key==='ArrowDown'?1:-1;
8963      this.setState({rcPlaceIndex:((this.state.rcPlaceIndex ?? -1)+direction+items.length)%items.length,rcPlaceOpen:true});
8964    } else if(event.key==='Enter' && this.state.rcPlaceIndex>=0){event.preventDefault();this.selectCabanaRematePlace(items[this.state.rcPlaceIndex]);}
8965  }
8966  toggleRosganRemateField(key) {
8967    const form = this.state.rosganRemateForm || this.emptyRosganRemateForm();
8968    this.setRosganRemateField(key, !form[key]);
8969  }
8970  setRosganRemateFlyer(file) {
8971    if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview);
8972    if (!file) { this.setState({ rosganRemateFlyer: null, rosganRemateFlyerPreview: '' }); return; }
8973    if (!String(file.type || '').startsWith('image/')) { this.setState({ rosganRemateMessage: { ok: false, text: 'El flyer debe ser una imagen.' } }); return; }
8974    this.setState({ rosganRemateFlyer: file, rosganRemateFlyerPreview: URL.createObjectURL(file), rosganRemateMessage: null });
8975  }
8976  buildRosganRematePayload() {
8977    const form = this.state.rosganRemateForm || this.emptyRosganRemateForm();
8978    const assigned = this.rosganAssignedConsignataria(this.state.rosganLotConsignatarias || []);
8979    const mode = form.modoConsignataria === 'Todos' ?'Todos' : 'Indiv
8979idual';
8980    const consignatariaId = mode === 'Todos' ?null : (this.canManageUsers(this.state.apiUser) ?form.consignatariaId : (assigned?.id || ''));
8981    const localDate = form.fecha && form.hora ?new Date(`${form.fecha}T${form.hora}:00`) : null;
8982    return {
8983      titulo: String(form.titulo || '').trim(),
8984      fechaHoraRemate: localDate && !Number.isNaN(localDate.getTime()) ?localDate.toISOString() : '',
8985      cabezas: Number(form.cabezas), tipoRemate: String(form.tipoRemate || ''), modoConsignataria: mode,
8986      consignatariaId: consignatariaId ?String(consignatariaId) : null, lugar: String(form.lugar || '').trim(), provincia: form.provincia || '', localidad: form.localidad || '',
8987      canalRural: !!form.canalRural, cargaDeLotes: !!form.cargaDeLotes,
8988      closeRemate: !!form.closeRemate, informaIndice: !!form.informaIndice, mostrarComitentes: !!form.mostrarComitentes,
8989    };
8990  }
8991  async setRemateLotLoading(item, enabled) {
8992    if (!this.canManageUsers(this.state.apiUser)) throw new Error('Solo Admin y SysAdmin pueden cambiar la carga de lotes.');
8993    if (this.state.remateLotLoadingBusy) return null;
8994    const id = this.remateRouteId(item);
8995    this.setState({ remateLotLoadingBusy: true });
8996    try {
8997      const response = await this.apiFetch('/api/remates/' + encodeURIComponent(id));
8998      const fresh = response?.data || response;
8999      if (enabled && fresh.closeRemate === true) throw new Error('Para habilitar la carga de lotes, abrí el remate desde Editar remate.');
9000      const { cargaDeDatos, filtroComitentes, ocultarEstablecimiento, ...currentFields } = fresh;
9001      const payload = { ...currentFields, cargaDeLotes: !!enabled, mostrarComitentes: fresh.mostrarComitentes ?? false };
9002      await this.apiFetch('/api/remates/' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify(payload) });
9003      const result = await this.apiFetch('/api/remates/' + encodeURIComponent(id));
9004      const savedData = result?.data || result;
9005      const saved = { ...savedData, mostrarComitentes: savedData.mostrarComitentes ?? false };
9006      if (saved.cargaDeLotes !== enabled) throw new Error('No se pudo confirmar el cambio. Actualizá el remate.');
9007      Object.assign(item, saved);
9008      const current = this.state.currentRemate;
9009      this.setState({ ...(current && String(this.remateRouteId(current)) === String(id) ? { currentRemate: { ...current, ...saved } } : {}),
9010        rosganRemates: (this.state.rosganRemates || []).map(r => String(this.remateRouteId(r)) === String(id) ? { ...r, ...saved } : r) });
9011      this.showToast(enabled ? 'Carga de lotes habilitada.' : 'Carga de lotes cerrada.');
9012      return saved;
9013    } finally { this.setState({ remateLotLoadingBusy: false }); }
9014  }
9015  async setRosganRemateClosed(item, closed) {
9016    if (!this.canManageUsers(this.state.apiUser) || this.state.remateClosingBusy) return;
9017    const id = this.remateRouteId(item);
9018    this.setState({ remateClosingBusy: true });
9019    try {
9020      const response = await this.apiFetch('/api/remates/' + encodeURIComponent(id));
9021      const fresh = response?.data || response;
9022      const { cargaDeDatos, filtroComitentes, ocultarEstablecimiento, ...fields } = fresh;
9023      const payload = { ...fields, closeRemate: !!closed, mostrarComitentes: fresh.mostrarComitentes ?? false };
9024      await this.apiFetch('/api/remates/' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify(payload) });
9025      const result = await this.apiFetch('/api/remates/' + encodeURIComponent(id));
9026      const saved = result?.data || result;
9027      if (saved.closeRemate !== closed) throw new Error('No se pudo confirmar el estado del remate.');
9028      this.setState({ currentRemate: { ...this.state.currentRemate, ...saved }, rosganRemates: (this.state.rosganRemates || []).map(r => String(this.remateRouteId(r)) === String(id) ?{ ...r, ...saved } : r) });
9029      this.showToast(closed ?'Remate cerrado.' : 'Remate abierto.');
9030    } catch (error) { this.showToast(error.message || 'No se pudo cambiar el estado del remate.'); }
9031    finally { this.setState({ remateClosingBusy: false }); }
9032  }
9033  async saveRosganRemate() {
9034    if (this.state.rosganRemateSaving) return;
9035    if (!this.canManageUsers(this.state.apiUser)) { this.showToast('Solo administradores pueden guardar remates.'); return; }
9036    const payload = this.buildRosganRematePayload();
9037    const selectedType = this.rosganRemateType(payload.tipoRemate);
9038    const required = [
9039      [payload.titulo, 'Ingresá el título del remate.'], [payload.fechaHoraRemate, 'Elegí la fecha y la hora del remate.'],
9040      [String((this.state.rosganRemateForm || {}).cabezas ?? '').trim() === '' || (Number.isSafeInteger(payload.cabezas) && payload.cabezas > 0), 'Indicá una cantidad de cabezas entera mayor a cero o dejá el campo vacío para calcularla automáticamente.'], [selectedType, 'Elegí un tipo de remate activo.'],
9041      [payload.modoConsignataria === 'Todos' || payload.consignatariaId, 'Elegí la consignataria.'],
9042      [payload.provincia, 'Elegí la provincia.'], [payload.localidad, 'Elegí la localidad.'],
9043    ];
9044    if (payload.modoConsignataria === 'Todos' && !selectedType?.permiteModoTodos) {
9045      this.setState({ rosganRemateMessage: { ok: false, text: 'Este tipo de remate solo admite una consignataria individual.' } });
9046      return;
9047    }
9048    const invalid = required.find(([valid]) => !valid);
9049    if (invalid) { this.setState({ rosganRemateMessage: { ok: false, text: invalid[1] } }); return; }
9050    const editingId = this.state.rosganRemateEditingId;
9051    this.setState({ rosganRemateSaving: true, rosganRemateMessage: null });
9052    try {
9053      const response = await this.apiFetch(editingId ?'/api/remates/' + encodeURIComponent(editingId) : '/api/remates', {
9054        method: editingId ?'PUT' : 'POST', body: JSON.stringify(payload),
9055      });
9056      const responseData = response?.data ?? response ?? {};
9057      const root = { ...responseData, mostrarComitentes: responseData.mostrarComitentes ?? false };
9058      const remateId = String(root?.id || root?.remateId || editingId || '');
9059      if (remateId) this.setState({ rosganRemates: [{ ...payload, ...root, id: remateId }, ...(this.state.rosganRemates || []).filter(item => String(item.id) !== remateId)] });
9060      if (this.state.rosganRemateFlyer) {
9061        if (!remateId) throw new Error('El remate se guardó, pero el API no devolvió su ID para cargar el flyer.');
9062        const flyerBody = new FormData();
9063        flyerBody.append('flyer', this.state.rosganRemateFlyer);
9064        await this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/flyer', { method: 'POST', body: flyerBody });
9065      }
9066      if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview);
9067      this.setState({
9068        rosganRemateSaving: false, rosganRemateFormOpen: false, rosganRemateEditingId: '', rosganRemateForm: null,
9069        rosganRemateFlyer: null, rosganRemateFlyerPreview: '', rosganRemateMessage: null,
9070      }, () => this.go('remates'));
9071      this.showToast(editingId ?'Remate actualizado correctamente.' : 'Remate creado correctamente.');
9072      await this.loadRosganRemateAdmin(true);
9073    } catch (error) {
9074      const detail = error?.data?.errors ?Object.values(error.data.errors).flat().join(' ') : '';
9075      this.setState({ rosganRemateSaving: false, rosganRemateMessage: { ok: false, text: detail || error?.message || 'No se pudo guardar el remate.' } });
9076    }
9077  }
9078  async deleteRosganRemate(item) {
9079    if (!this.canManageUsers(this.state.apiUser)) { this.showToast('Solo administradores pueden eliminar remates.'); return; }
9080    const id = String(item?.id || '');
9081    if (!id || this.state.rosganRemateDeleting || !window.confirm(`¿Eliminar el remate "${item?.titulo || 'Rosgan'}"? Esta acción no se puede deshacer.`)) return;
9082    this.setState({ rosganRemateDeleting: id });
9083    try {
9084      await this.apiFetch('/api/Remates/' + encodeURIComponent(id), { method: 'DELETE' });
9085      this.setState({ rosganRemateDeleting: '' });
9086      this.showToast('Remate eliminado.');
9087      if (this.state.currentRemate?.__rosganActive && this.remateRouteId(this.state.currentRemate) === id) this.go('remates');
9088      await this.loadRosganRemateAdmin(true);
9089    } catch (error) {
9090      this.setState({ rosganRemateDeleting: '', rosganRematesError: error?.message || 'No se pudo eliminar el remate.' });
9091    }
9092  }
9093  rosganConsignatariasForRemate(remateId = this.rosganForm().remateId) {
9094    if (!this.state.user?.uid || !this.state.apiUser || this.state.rosganLotLoading || this.state.rosganLotScopeUid !== this.state.user.uid) return [];
9095    const remate = (this.state.rosganLotRemates || []).find((item) => String(item.id) === String(remateId));
9096    const all = this.state.rosganLotConsignatarias || [];
9097    const participants = Array.isArray(remate?.consignatariasParticipantes) ?remate.consignatariasParticipantes : [];
9098    let list = participants.length ?participants.map((participant) => {
9099      const id = participant.consignatariaId || participant.id;
9100      return all.find((item) => String(item.id) === String(id)) || { id, name: participant.consignatariaNombre || participant.name || id };
9101    }) : (remate?.consignatariaId ?all.filter((item) => String(item.id) === String(remate.consignatariaId)) : []);
9102    const assigned = this.rosganAssignedConsignatarias(all);
9103    if (assigned) list = list.filter((item) => assigned.some((member) => this.consKey(item.id || item.name) === this.consKey(member.id || member.name)));
9104    return list;
9105  }
9106  async loadRosganLotCatalogs(force = false) {
9107    const uid = this.state.user?.uid;
9108    if (!uid || !this.state.apiUser) return;
9109    this.loadRosganLotOptions(force);
9110    if (this.state.rosganLotLoading && this._rosganCatalogUid === uid) return;
9111    if (!force && this.state.rosganLotScopeUid === uid && this.state.rosganLotRemates && this.state.rosganLotCategories) return;
9112    this._rosganCatalogUid = uid;
9113    const request = this._rosganCatalogRequest = (this._rosganCatalogRequest || 0) + 1;
9114    const currentRequest = () => request === this._rosganCatalogRequest && uid === this.state.user?.uid;
9115    this.setState({ rosganLotLoading: true, rosganLotScopeUid: '', rosganLotRelationsScope: '', rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganConsignatariaMenuOpen: false, rosganComitenteMenuOpen: false, rosganLotMessage: null });
9116    try {
9117      const relations = this.canManageUsers(this.state.apiUser) ?[] : await this.fetchUserConsignataria(uid);
9118      if (!currentRequest()) return;
9119      const [rematesRaw, categoriesRaw, breedsRaw, consignatariasRaw] = await Promise.all([
9120        this.apiFetch('/api/remates'), this.apiFetch('/api/rosgan-categories'),
9121        this.apiFetch('/api/rosganBreeds'), this.apiFetch('/api/rosgan-consignatarias'),
9122      ]);
9123      const remates = this.rosganEligibleRemates(this.rosganArray(rematesRaw));
9124      const categories = this.rosganArray(categoriesRaw).filter((item) => item?.isActive !== false)
9125        .sort((a, b) => Number(a.order || 0) - Number(b.order || 0));
9126      const breeds = this.rosganArray(breedsRaw).filter((item) => item?.isActive !== false)
9127        .sort((a, b) => Number(a.order || 0) - Number(b.order || 0));
9128      if (!currentRequest()) return;
9129      let consignatarias = this.rosganArray(consignatariasRaw).filter((item) => item?.isActive !== false && item?.activo !== false);
9130      const current = this.rosganForm();
9131      const assigned = this.rosganAssignedConsignatarias(consignatarias, relations || []);
9132      if (assigned) consignatarias = assigned;
9133      const next = { ...current };
9134      const availableRemates = this.canManageUsers(this.state.apiUser) ?remates : remates.filter((item) => {
9135        const participants = Array.isArray(item.consignatariasParticipantes) && item.consignatariasParticipantes.length
9136          ?item.consignatariasParticipantes.map((entry) => entry.consignatariaId || entry.id)
9137          : [item.consignatariaId].filter(Boolean);
9138        return participants.some((id) => consignatarias.some((member) => String(member.id) === String(id)));
9139      });
9140      if (!next.remateId && availableRemates.length === 1) next.remateId = String(availableRemates[0].id);
9141      if (!availableRemates.some((item) => String(item.id) === String(next.remateId))) next.remateId = '';
9142      if (assigned?.length === 1) next.consignatariaId = String(assigned[0].id);
9143      if (!consignatarias.some(item => String(item.id) === String(next.consignatariaId))) next.consignatariaId = '';
9144      if (next.consignatariaId !== current.consignatariaId) {
9145        next.certifiedBy = null;
9146        next.comitenteId = ''; next.establishmentId = '';
9147      }
9148      this.setState({
9149        rosganLotRemates: availableRemates, rosganLotCategories: categories, rosganLotBreeds: breeds,
9150        rosganLotConsignatarias: consignatarias, rosganLotForm: next, rosganLotLoading: false, rosganLotScopeUid: uid, rosganComitenteSearch: '',
9151      }, () => this.loadRosganLotRelations());
9152    } catch (error) {
9153      if (!currentRequest()) return;
9154      this.setState({ rosganLotLoading: false, rosganLotMessage: { ok: false, text: error?.message || 'No se pudieron cargar los catálogos de Rosgan.' } });
9155    }
9156  }
9157  rosganLotRelationScope() {
9158    const id = String(this.rosganForm().consignatariaId || '');
9159    return id && this.rosganConsignatariasForRemate().some(item => String(item.id) === id) ?this.state.user.uid + ':' + id : '';
9160  }
9161  async loadRosganCertifiers() {
9162    const id = String(this.rosganForm().consignatariaId || '');
9163    const request = this._rosganCertifiersRequest = (this._rosganCertifiersRequest || 0) + 1;
9164    this.setState({ rosganCertifiers: [], rosganCertifiersLoading: !!id, rosganCertifiersError: '', rosganCertifierMenuOpen: false });
9165    if (!id) return;
9166    const isCurrent = () => request === this._rosganCertifiersRequest && id === String(this.rosganForm().consignatariaId || '');
9167    try {
9168      const result = await this.apiFetch('/api/consignatarias/' + encodeURIComponent(id) + '/certifiers');
9169      if (!isCurrent()) return;
9170      this.setState({ rosganCertifiers: this.rosganArray(result), rosganCertifiersLoading: false });
9171    } catch (error) {
9172      if (!isCurrent()) return;
9173      this.setState({ rosganCertifiersLoading: false, rosganCertifiersError: error?.message || 'No se pudieron obtener los responsables comerciales' });
9174    }
9175  }
9176  rosganOperationComitenteId(item) {
9177    return String(item?.operationComitenteId ?? item?.canonicalComitenteId ?? item?.mergedIntoId ?? item?.id ?? '');
9178  }
9179  rosganSelectedOperationComitenteId() {
9180    const f = this.rosganForm();
9181    const item = [...(this.state.rosganLotComitentes || []), ...(this.state.rosganLotPendingComitentes || [])].find(c => String(c.id) === String(f.comitenteId) && (!c.consignatariaId || String(c.consignatariaId) === String(f.consignatariaId)));
9182    return this.rosganOperationComitenteId(item || { id: f.comitenteId });
9183  }
9184  rosganCanonicalComitentes(comitentes = [], pending = []) {
9185    const canonicalId = item => this.rosganOperationComitenteId(item);
9186    const result = new Map();
9187    const main = [...comitentes].sort((a, b) => Number(String(a.id) !== canonicalId(a)) - Number(String(b.id) !== canonicalId(b)));
9188    for (const item of [...main.map(item => ({ ...item, validated: true })), ...pending.filter(item => !item.mergedIntoId && !item.canonicalComitenteId)]) {
9189      const id = canonicalId(item);
9190      if (id && !result.has(id)) result.set(id, { ...item });
9191    }
9192    return [...result.values()];
9193  }
9194  rosganMergeEstablishments(consignatariaId, ...lists) {
9195    const result = new Map();
9196    for (const list of lists) for (const item of this.rosganArray(list)) {
9197      const id = String(item.id || item.establishmentId || item.establecimientoId || '');
9198      const owner = String(item.consignatariaId || consignatariaId);
9199      if (!id || owner !== String(consignatariaId)) continue;
9200      result.set(id, { ...item, id, consignatariaId: owner, comitenteId: String(item.comitenteHistoricoId || item.comitenteId || '') });
9201    }
9202    return [...result.values()];
9203  }
9204  async loadRosganLotRelations({ preserve = false } = {}) {
9205    this.loadRosganCertifiers();
9206    const scope = this.rosganLotRelationScope();
9207    const consignatariaId = String(this.rosganForm().consignatariaId || '');
9208    const request = this._rosganRelationsRequest = (this._rosganRelationsRequest || 0) + 1;
9209    if (!preserve) this.setState({ rosganLotRelationsScope: '', rosganLotRelationsLoading: !!scope, rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganComitenteMenuOpen: false, rosganEstablishmentMenuOpen:false, rosganEstablishmentSearch:'' });
9210    if (!consignatariaId || !scope) return;
9211    const currentRequest = () => request === this._rosganRelationsRequest && scope === this.rosganLotRelationScope();
9212    try {
9213      const response = await this.apiFetch('/api/legacy-comitentes?mode=relations&consignatariaId=' 
9213+ encodeURIComponent(consignatariaId));
9214      const relations = response?.data ?? response;
9215      if (!currentRequest()) return;
9216      this.setState({
9217        rosganLotPendingComitentes: [],
9218        rosganComitenteLimit: 60,
9219        rosganLotComitentes: relations.comitentes ?? [],
9220        rosganLotEstablishments: [...(relations.establishments ?? []), ...(relations.establecimientosHistorico ?? [])],
9221        rosganLotRelationsLoading: false, rosganLotRelationsScope: scope,
9222      });
9223    } catch (error) {
9224      if (!currentRequest()) return;
9225      if (preserve) { this.setState({ rosganLotRelationsLoading: false }); return; }
9226      this.setState({ rosganLotComitentes: [], rosganLotEstablishments: [], rosganLotRelationsLoading: false, rosganLotMessage: { ok: false, text: error?.message || 'No se pudieron cargar comitentes y establecimientos.' } });
9227    }
9228  }
9229  rosganComitentesEndpoint(query = '') {
9230    // Catálogo compartido con Roslot. Para seleccionar relaciones consultar por consignatariaId;
9231    // el backend debe validar la pertenencia también al crear un lote Rosgan.
9232    return '/api/Comitentes' + String(query || '');
9233  }
9234  async openComitenteMerge(fromLot = false) {
9235    if (!this.canManageComitentes(this.state.apiUser)) return;
9236    let consignatarias = fromLot ? this.rosganConsignatariasForRemate() : (this.state.userConsignatarias || []);
9237    if (!fromLot && this.canManageUsers(this.state.apiUser)) {
9238      try { consignatarias = this.rosganArray(await this.apiFetch('/api/rosgan-consignatarias')); }
9239      catch (error) { this.showToast(error.message || 'No se pudieron cargar las consignatarias.'); return; }
9240    }
9241    window.RosganComitenteMerge.open({
9242      consignatarias,
9243      consignatariaId: fromLot ? this.rosganForm().consignatariaId : '',
9244      apiFetch: this.apiFetch.bind(this),
9245      onSuccess: async ({ consignatariaId, canonicalComitenteId, duplicateComitenteIds }) => {
9246        this.showToast('Comitentes fusionados correctamente.');
9247        this._profileComitentesRequest = (this._profileComitentesRequest || 0) + 1;
9248        this._profileEstablishmentsRequest = (this._profileEstablishmentsRequest || 0) + 1;
9249        this._rosganRelationsRequest = (this._rosganRelationsRequest || 0) + 1;
9250        const form = this.rosganForm();
9251        const sameScope = String(form.consignatariaId) === consignatariaId;
9252        const keepCanonical = sameScope && [canonicalComitenteId, ...duplicateComitenteIds].includes(String(form.comitenteId));
9253        this.setState({ comitentes: null, comitentesScope: '', establishments: null, establishmentsLoading: false,
9254          rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganLotRelationsScope: '',
9255          ...(keepCanonical ? { rosganLotForm: { ...form, comitenteId: canonicalComitenteId }, rosganComitenteSearch: '' } : {}),
9256        });
9257        // Refresh the exact relations endpoint even when the profile is the entry point.
9258        if (sameScope) await this.loadRosganLotRelations();
9259        else {
9260          try { await this.apiFetch('/api/legacy-comitentes?mode=relations&consignatariaId=' + encodeURIComponent(consignatariaId), { cache: 'no-store' }); }
9261          catch { this.showToast('Fusión completada. No se pudo actualizar la lista; volvé a consultarla.'); }
9262        }
9263        if (!fromLot) await Promise.all([this.fetchComitentes(), this.fetchEstablishments()]);
9264      },
9265    });
9266  }
9267  roslotCuitLookupEndpoint(cuit) {
9268    return this.rosganComitentesEndpoint('/lookup-by-cuit?cuit=' + encodeURIComponent(String(cuit || '').replace(/\D/g, '')));
9269  }
9270  onRosganRemateChange(remateId) {
9271    this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1;
9272    this._rosganComitenteLookupKey = '';
9273    const allowed = this.rosganConsignatariasForRemate(remateId);
9274    const currentConsignataria = this.rosganForm().consignatariaId;
9275    const keepCurrent = allowed.some((item) => String(item.id) === String(currentConsignataria));
9276    const consignatariaId = keepCurrent ?currentConsignataria : (allowed.length === 1 ?allowed[0].id : '');
9277    this.setState({
9278      rosganLotForm: { ...this.rosganForm(), remateId, consignatariaId: String(consignatariaId || ''), certifiedBy: keepCurrent ?this.rosganForm().certifiedBy : null, comitenteId: '', establishmentId: '' },
9279      rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganLotMessage: null, rosganConsignatariaMenuOpen: false,
9280      rosganComitenteFormOpen: false, rosganComitenteMessage: null, rosganComitenteSearch: '', rosganComitenteMenuOpen: false,
9281      rosganComitenteLookupLoading: false,
9282      rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' },
9283    }, () => this.loadRosganLotRelations());
9284  }
9285  onRosganConsignatariaChange(consignatariaId) {
9286    if (!this.rosganConsignatariasForRemate().some(item => String(item.id) === String(consignatariaId))) return;
9287    this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1;
9288    this._rosganComitenteLookupKey = '';
9289    this.setState({
9290      rosganLotForm: { ...this.rosganForm(), consignatariaId, certifiedBy: null, comitenteId: '', establishmentId: '' },
9291      rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganLotMessage: null, rosganConsignatariaMenuOpen: false,
9292      rosganComitenteFormOpen: false, rosganComitenteMessage: null, rosganComitenteSearch: '', rosganComitenteMenuOpen: false,
9293      rosganComitenteLookupLoading: false,
9294      rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' },
9295    }, () => this.loadRosganLotRelations());
9296  }
9297  toggleRosganComitenteForm() {
9298    if (!this.canManageComitentes(this.state.apiUser)) {
9299      this.setState({ rosganComitenteFormOpen: false, rosganLotMessage: { ok: false, text: 'No tenés permisos para gestionar comitentes.' } });
9300      return;
9301    }
9302    this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1;
9303    this._rosganComitenteLookupKey = '';
9304    this.setState({
9305      rosganComitenteFormOpen: !this.state.rosganComitenteFormOpen,
9306      rosganComitenteLookupLoading: false,
9307      rosganComitenteMessage: null,
9308      rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' },
9309    });
9310  }
9311  setRosganComitenteField(key, value, buffered = false) {
9312    const next = { ...this.state.rosganComitenteForm, [key]: value };
9313    if (key === 'province') {
9314      next.locality = '';
9315      if (value) this.fetchLocalities(value);
9316    }
9317    const patch = { rosganComitenteForm: next, rosganComitenteMessage: null };
9318    if (buffered) this.bufferInputState('rosgan-comitente-' + key, patch);
9319    else this.setState(patch);
9320  }
9321  onRosganComitenteCuit(value) {
9322    const cuit = String(value || '').replace(/\D/g, '').slice(0, 11);
9323    if (cuit.length < 11) {
9324      this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1;
9325      this._rosganComitenteLookupKey = '';
9326    }
9327    this.setState({
9328      rosganComitenteForm: { ...this.state.rosganComitenteForm, cuit, source: '' },
9329      rosganComitenteLookupLoading: false,
9330      rosganComitenteMessage: null,
9331    }, () => { if (cuit.length === 11) this.lookupRosganComitente(cuit); });
9332  }
9333  async fetchRosganComitenteLookup(cuit) {
9334    const consignatariaId = String(this.rosganForm().consignatariaId || '');
9335    const scope = this.rosganLotRelationScope();
9336    if (!consignatariaId || !scope) throw new Error('Primero seleccioná una consignataria habilitada.');
9337    let response;
9338    try {
9339      response = await this.apiFetch(`/api/Comitentes/lookup?consignatariaId=${encodeURIComponent(consignatariaId)}&cuit=${encodeURIComponent(cuit)}`);
9340    } catch (error) {
9341      if (Number(error?.status) !== 404) throw error;
9342    }
9343    if (scope !== this.rosganLotRelationScope()) throw new Error('La consignataria cambió. Volvé a consultar el CUIT.');
9344    const data = response?.data ?? response;
9345    const record = data?.comitente || data?.item || data;
9346    if (data?.found !== false && (record?.id || record?.comitenteId)) {
9347      if (String(record.consignatariaId || '') !== consignatariaId) throw new Error('El comitente recibido no pertenece a la consignataria seleccionada.');
9348      const recordCuit = String(record.cuitCuil || record.cuitNormalized || record.cuit || '').replace(/\D/g, '');
9349      if (recordCuit !== cuit) throw new Error('El comitente recibido no coincide con el CUIT consultado.');
9350      return { data: { ...data, comitente: record, source: 'local', found: true } };
9351    }
9352    return this.apiFetch(this.roslotCuitLookupEndpoint(cuit));
9353  }
9354  async lookupRosganComitente(cuit) {
9355    if (!this.canManageComitentes(this.state.apiUser)) return;
9356    const normalized = String(cuit || '').replace(/\D/g, '');
9357    if (normalized.length !== 11) return;
9358    const lookupKey = normalized;
9359    if (this._rosganComitenteLookupKey === lookupKey && this.state.rosganComitenteLookupLoading) return;
9360    this._rosganComitenteLookupKey = lookupKey;
9361    const token = (this._rosganComitenteLookupToken || 0) + 1;
9362    this._rosganComitenteLookupToken = token;
9363    this.setState({ rosganComitenteLookupLoading: true, rosganComitenteMessage: { kind: 'info', text: 'Consultando CUIT en ARCA…' } });
9364    try {
9365      const response = await this.fetchRosganComitenteLookup(normalized);
9366      if (token !== this._rosganComitenteLookupToken) return;
9367      const data = response?.data ?? response ?? {};
9368      if (data?.error) throw new Error(String(data.error));
9369      const record = data?.comitente || data?.item || data;
9370      const found = data?.found === true || (data?.found !== false && Boolean(
9371        record?.cuitCuil || record?.cuit || record?.cuitNormalized || record?.nombreFiscal || record?.fiscalName || record?.razonSocial || record?.alias || record?.name || record?.nombre
9372      ));
9373      if (!found) {
9374        this.setState({
9375          rosganComitenteLookupLoading: false,
9376          rosganComitenteForm: { ...this.state.rosganComitenteForm, source: '' },
9377          rosganComitenteMessage: { kind: 'error', text: 'ARCA no encontró este CUIT. Revisá los 11 dígitos.' },
9378        });
9379        return;
9380      }
9381      const source = String(data.source || record?.source || '').toLowerCase();
9382      const current = this.state.rosganComitenteForm || {};
9383      const provinceOptions = this.state.geoProvinces?.length ?this.state.geoProvinces : ARGENTINA_PROVINCES;
9384      const provinceRaw = String(record.province || record.provincia || '').trim();
9385      const isCaba = ['caba', 'capitalfederal', 'ciudadautonomabuenosaires', 'ciudadautonomadebuenosaires'].includes(this.consKey(provinceRaw));
9386      const province = isCaba ? 'Ciudad Autónoma de Buenos Aires' : (provinceOptions.find((item) => this.consKey(item) === this.consKey(provinceRaw)) || provinceRaw);
9387      // ARCA may omit localidad for CABA: province and city are the same jurisdiction.
9388      const locality = String(record.locality || record.localidad || '').trim() || (isCaba ? 'Ciudad Autónoma de Buenos Aires' : '');
9389      const relationScope = this.rosganLotRelationScope();
9390      const ownItems = relationScope && this.state.rosganLotRelationsScope === relationScope
9391        ? (this.state.rosganLotComitentes || []) : [];
9392      const localItem = ownItems.find((item) => {
9393        const itemCuit = String(item?.cuitCuil || item?.cuitNormalized || item?.cuit || '').replace(/\D/g, '');
9394        return itemCuit === normalized;
9395      });
9396      const existingId = String(localItem?.id || localItem?.comitenteId || '');
9397      const fiscalName = String(record?.nombreFiscal || record?.fiscalName || record?.razonSocial || localItem?.nombreFiscal || localItem?.fiscalName || current.fiscalName || '').trim();
9398      const name = fiscalName || String(record?.alias || record?.name || record?.nombre || localItem?.alias || localItem?.name || localItem?.nombre || current.name || '').trim();
9399      if (province) this.fetchLocalities(province);
9400      if (localItem && existingId) {
9401        this.setState({
9402          rosganLotForm: { ...this.rosganForm(), comitenteId: existingId, establishmentId: '' },
9403          rosganComitenteSearch: localItem?.alias || record?.alias || localItem?.name || record?.name || name || fiscalName || normalized,
9404          rosganComitenteMenuOpen: false,
9405          rosganComitenteFormOpen: false,
9406          rosganComitenteLookupLoading: false,
9407          rosganComitenteMessage: null,
9408          rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' },
9409          rosganLotMessage: { ok: true, text: 'Comitente existente encontrado y seleccionado.' },
9410        });
9411        return;
9412      }
9413      const verifiedSource = source || 'arca';
9414      this.setState({
9415        rosganComitenteLookupLoading: false,
9416        rosganComitenteForm: { ...current, cuit: normalized, name, fiscalName, province, locality, source: verifiedSource },
9417        rosganComitenteMessage: {
9418          kind: name && province && locality ? 'success' : 'warning',
9419          text: name && province && locality
9420            ? (source === 'local' ? 'Comitente encontrado en los registros locales.' : 'Razón social y domicilio fiscal obtenidos de ARCA.')
9421            : 'La consulta terminó, pero no devolvió: ' + [!name && 'razón social', !province && 'provincia fiscal', !locality && 'localidad fiscal'].filter(Boolean).join(', ') + '. Revisá los datos fiscales antes de crear el comitente.',
9422        },
9423      });
9424    } catch (error) {
9425      if (token !== this._rosganComitenteLookupToken) return;
9426      const accessDenied = Number(error?.status) === 401 || Number(error?.status) === 403;
9427      const backendMessage = String(error?.data?.message || error?.message || '').trim();
9428      this.setState({
9429        rosganComitenteLookupLoading: false,
9430        rosganComitenteForm: { ...this.state.rosganComitenteForm, source: '' },
9431        rosganComitenteMessage: accessDenied
9432          ?{ kind: 'error', text: backendMessage || 'No tenés permisos para gestionar comitentes.' }
9433          :{ kind: 'error', text: backendMessage || 'ARCA no respondió. Reintentá la consulta.' },
9434      });
9435    }
9436  }
9437  onRosganComitenteSearch(value) {
9438    this.bufferInputState('rosgan-comitente-search', {
9439      rosganComitenteSearch: value,
9440      rosganComitenteMenuOpen: true,
9441      rosganLotForm: { ...this.rosganForm(), comitenteId: '', establishmentId: '' },
9442    }, 100);
9443  }
9444  selectRosganComitente(item) {
9445    const scope = this.rosganLotRelationScope();
9446    if (!scope || scope !== this.state.rosganLotRelationsScope || this.state.rosganLotRelationsLoading) return;
9447    const id = String(item?.id || item?.comitenteId || '');
9448    const name = item?.alias || item?.name || item?.nombre || item?.nombreFiscal || item?.fiscalName || item?.razonSocial || id;
9449    clearTimeout(this._rosganComitenteCloseTimer);
9450    // The renderer preserves focused input values while typing. A confirmed
9451    // selection must replace that query with the complete alias immediately.
9452    const input = document.getElementById('rg-comitente-search');
9453    if (input) input.value = String(name || '');
9454    this.setState({
9455      rosganComitenteSearch: String(name || ''),
9456      rosganComitenteMenuOpen: false,
9457      rosganLotForm: { ...this.rosganForm(), comitenteId: id, establishmentId: '' },
9458      rosganLotMessage: null,
9459    });
9460  }
9461  openRosganComitenteMenu() {
9462    clearTimeout(this._rosganComitenteCloseTimer);
9463    if (!this.state.rosganComitenteMenuOpen) {
9464      // Do not replace the input during the browser's focus/typing sequence.
9465      this.bufferInputState('rosgan-comitente-search', { rosganComitenteMenuOpen: true }, 100);
9466    }
9467  }
9468  closeRosganComitenteMenu(event) {
9469    const picker = event?.currentTarget?.closest('[data-rg-comitente-picker]');
9470    if (picker && event.relatedTarget && picker.contains(event.relatedTarget)) return;
9471    clearTimeout(this._rosganComitenteCloseTimer);
9472    this._rosganComitenteCloseTimer = setTimeout(() => {
9473      this._rosganComitenteCloseTimer = null;
9474      if (this.state.rosganComitenteMenuOpen) this.setState({ rosganComitenteMenuOpen: false });
9475    }, 140);
9476  }
9477  async saveRosganComitente() {
9478    if (this.state.rosganComitenteSaving) return;
9479    if (this.state.rosganComitenteLookupLoading) return;
9480    const scope = this.rosganLotRelationScope();
9481    const consignatariaId = String(this.rosganForm().consignatariaId || '');
9482    if (!scope) return;
9483    if (!this.canManageComitentes(this.state.apiUser)) {
9484      this.setState({ rosganComitenteFormOpen: false, rosganLotMessage: { ok: false, text: 'No tenés permisos para gestionar comitentes.' } });
9485      return;
9486    }
9487    const form = this.state.rosganComitenteForm || {};
9488    if (!String(form.alias || '').trim()) { this.setState({ rosganComitenteMessage: { ok: false, text: 'Ingresá el al
9488ias del comitente.' } }); return; }
9489    const name = String(form.fiscalName || form.name || form.alias || '').trim();
9490    const cuit = String(form.cuit || '').replace(/\D/g, '');
9491    this.setState({ rosganComitenteSaving: true, rosganComitenteMessage: null });
9492    try {
9493      const created = await this.apiFetch(form.source && cuit ? '/api/Comitentes' : '/api/legacy-comitentes', {
9494        method: 'POST',
9495        headers: { 'X-Rosgan-Catalog': 'true' },
9496        body: JSON.stringify({
9497          ...(!form.source || !cuit ? {action:'create_alias'} : {}),
9498          cuit,
9499          consignatariaId,
9500          name,
9501          alias: String(form.alias || '').trim(),
9502          fiscalName: String(form.fiscalName || '').trim(),
9503          province: String(form.province || '').trim(),
9504          locality: String(form.locality || '').trim(),
9505        }),
9506      });
9507      const refreshed = [...(this.state.rosganLotComitentes || [])];
9508      if (scope !== this.rosganLotRelationScope()) { this.setState({ rosganComitenteSaving: false }); return; }
9509      const createdId = String(created?.comitente?.id || created?.id || created?.comitenteId || created?.data?.comitente?.id || created?.data?.id || created?.data?.comitenteId || '');
9510      const record = created?.comitente || created?.data?.comitente || created?.data || created;
9511      const match = record;
9512      const selectedId = createdId;
9513      if (!selectedId) throw new Error('La API no devolvió el ID del comitente creado. No vuelvas a crearlo; actualizá el catálogo.');
9514      if (!refreshed.some(item => String(item.id || item.comitenteId) === selectedId)) {
9515        refreshed.push({ id: selectedId, consignatariaId, name, alias: String(form.alias || '').trim(), cuit, fiscalName: form.fiscalName, province: form.province, locality: form.locality, ...record, id: selectedId });
9516      }
9517      this.setState({
9518        rosganLotComitentes: refreshed,
9519        rosganLotRelationsScope: scope,
9520        rosganLotForm: { ...this.rosganForm(), comitenteId: selectedId, establishmentId: '' },
9521        rosganComitenteFormOpen: false,
9522        rosganComitenteSaving: false,
9523        rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' },
9524        rosganComitenteSearch: String(match?.alias || match?.name || match?.nombre || name),
9525        rosganComitenteMenuOpen: false,
9526        rosganComitenteMessage: null,
9527        rosganLotMessage: { ok: true, text: 'Comitente creado y seleccionado.' },
9528      });
9529    } catch (error) {
9530      const detail = error?.data?.errors && typeof error.data.errors === 'object' ?Object.values(error.data.errors).flat().filter(Boolean)[0] : '';
9531      const message = String(detail || error?.message || 'No se pudo crear el comitente.').replace(/CUIT\s*\/\s*CUIL/gi, 'CUIT').replace(/\s+para esta consignataria\.?/gi, '.');
9532      this.setState({ rosganComitenteSaving: false, rosganComitenteMessage: { ok: false, text: message } });
9533    }
9534  }
9535  emptyRosganEstablishmentForm() {
9536    return { nombre: '', provincia: '', localidad: '', renspa: '', latitud: '', longitud: '' };
9537  }
9538  toggleRosganEstablishmentForm() {
9539    if (!this.canManageComitentes(this.state.apiUser)) {
9540      this.setState({ rosganEstablishmentFormOpen: false, rosganLotMessage: { ok: false, text: 'No tenés permisos para gestionar establecimientos.' } });
9541      return;
9542    }
9543    if (!this.rosganForm().comitenteId) {
9544      this.setState({ rosganLotMessage: { ok: false, text: 'Primero creá o seleccioná un comitente.' } });
9545      return;
9546    }
9547    const opening = !this.state.rosganEstablishmentFormOpen;
9548    if (opening) this.fetchProvinces();
9549    this.setState({
9550      rosganEstablishmentFormOpen: opening,
9551      rosganEstablishmentForm: opening ?this.emptyRosganEstablishmentForm() : this.state.rosganEstablishmentForm,
9552      rosganEstablishmentMessage: null, rosganEstPlaceResults: [], rosganEstPlaceError: '', rosganEstPlaceLoading: false,
9553    });
9554  }
9555  setRosganEstablishmentField(key, value) {
9556    const next = { ...(this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm()), [key]: value };
9557    if (key === 'provincia') {
9558      next.localidad = ''; next.latitud = ''; next.longitud = '';
9559    }
9560    this.setState({ rosganEstablishmentForm: next, rosganEstablishmentMessage: null, ...(key === 'provincia' ?{ rosganEstPlaceResults: [], rosganEstPlaceError: '' } : {}) });
9561  }
9562  onRosganEstPlaceInput(value) {
9563    const localidad = String(value || '');
9564    const form = { ...(this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm(
9564)), localidad, latitud: '', longitud: '' };
9565    this.setState({ rosganEstablishmentForm: form, rosganEstablishmentMessage: null, rosganEstPlaceError: '', rosganEstPlaceResults: localidad.trim().length >= 3 ?this.state.rosganEstPlaceResults : [] });
9566    clearTimeout(this._rosganEstPlaceTimer);
9567    if (!form.provincia || localidad.trim().length < 3) return;
9568    this._rosganEstPlaceTimer = setTimeout(() => this.searchRosganEstPlaces(localidad, form.provincia), 280);
9569  }
9570  async searchRosganEstPlaces(localidad, provincia) {
9571    const query = String(localidad || '').trim();
9572    if (query.length < 3) return;
9573    const token = (this._rosganEstPlaceToken || 0) + 1;
9574    this._rosganEstPlaceToken = token;
9575    this.setState({ rosganEstPlaceLoading: true, rosganEstPlaceError: '' });
9576    try {
9577      const response = await this.apiFetch('/api/places/search', { method: 'POST', body: JSON.stringify({ query, provincia: String(provincia || '').trim() }) });
9578      if (token !== this._rosganEstPlaceToken) return;
9579      const results = Array.isArray(response?.data) ?response.data : this.listFrom(response, ['data', 'items', 'results']);
9580      this.setState({ rosganEstPlaceLoading: false, rosganEstPlaceResults: results.slice(0, 7), rosganEstPlaceError: results.length ?'' : 'No encontramos esa localidad. Podés escribirla manualmente.' });
9581    } catch {
9582      if (token !== this._rosganEstPlaceToken) return;
9583      this.setState({ rosganEstPlaceLoading: false, rosganEstPlaceResults: [], rosganEstPlaceError: 'Google no respondió. Podés escribir la localidad manualmente.' });
9584    }
9585  }
9586  selectRosganEstPlace(item) {
9587    if (!item) return;
9588    this.setState({
9589      rosganEstablishmentForm: { ...(this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm()), localidad: String(item.label || item.address || ''), latitud: item.lat != null ?String(item.lat) : '', longitud: item.lng != null ?String(item.lng) : '' },
9590      rosganEstPlaceResults: [], rosganEstPlaceError: '', rosganEstablishmentMessage: null,
9591    });
9592  }
9593  closeRosganEstPlaceResults() {
9594    setTimeout(() => this.setState({ rosganEstPlaceResults: [] }), 150);
9595  }
9596  selectRosganEstablishment(establishmentId) {
9597    if (!this.rosganForm().comitenteId) { this.showToast('Primero seleccioná un comitente.'); return; }
9598    this.setState({ rosganEstablishmentMenuOpen:false, rosganEstablishmentSearch:'' });
9599    const item = (this.state.rosganLotEstablishments || []).find((entry) => String(entry.id || entry.establishmentId || entry.establecimientoId || '') === String(establishmentId || ''));
9600    if (!item) { this.setState({ rosganLotForm: { ...this.rosganForm(), establishmentId: '' } }); return; }
9601    if (item.isActive === false || String(item.comitenteId || '') !== this.rosganSelectedOperationComitenteId()) return;
9602    this.setState({ rosganLotForm: { ...this.rosganForm(), establishmentId:String(item.id) }, rosganLotMessage:null });
9603  }
9604  async saveRosganEstablishment() {
9605    if (this.state.rosganEstablishmentSaving) return;
9606    const scope = this.rosganLotRelationScope();
9607    if (!scope) return;
9608    const consignatariaId = String(this.rosganForm().consignatariaId || '');
9609    const comitenteId = this.rosganSelectedOperationComitenteId();
9610    const form = this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm();
9611    if (!comitenteId) { this.setState({ rosganEstablishmentMessage: { ok: false, text: 'Primero seleccioná un comitente.' } }); return; }
9612    if (!String(form.nombre || '').trim() || !String(form.provincia || '').trim() || !String(form.localidad || '').trim()) {
9613      this.setState({ rosganEstablishmentMessage: { ok: false, text: 'Completá nombre, provincia y localidad.' } }); return;
9614    }
9615    this.setState({ rosganEstablishmentSaving: true, rosganEstablishmentMessage: null });
9616    let savedEstablishmentId = '';
9617    try {
9618      const body = {
9619        consignatariaId, comitenteId,
9620        name: String(form.nombre).trim(), province: String(form.provincia).trim(), locality: String(form.localidad).trim(),
9621        renspa: String(form.renspa || '').trim() || null, location: null,
9622        latitude: this.numOrNull(form.latitud), longitude: this.numOrNull(form.longitud),
9623      };
9624      const created = await this.apiFetch('/api/rosgan-establishments', { method: 'POST', body: JSON.stringify(body) });
9625      if (scope !== this.rosganLotRelationScope()) { this.setState({ rosganEstablishmentSaving: false }); return; }
9626      const saved = created?.establishment || created?.establecimiento || created?.data?.establishment || created?.data?.establecimiento || created?.data || created;
9627      const createdId = String(saved?.id || created?.establishmentId || created?.establecimientoId || '');
9628      const selectedId = createdId;
9629      if (!selectedId) throw new Error('La API no devolvió el ID del establecimiento creado. No vuelvas a crearlo; actualizá el catálogo.');
9630      savedEstablishmentId = selectedId;
9631      const resolvedComitenteId = String(saved?.comitenteId || comitenteId);
9632      const establishments = [...(this.state.rosganLotEstablishments || []).filter(item => String(item.id) !== selectedId), { ...saved, id: selectedId, consignatariaId, comitenteId: resolvedComitenteId }];
9633      this.setState({
9634        rosganLotEstablishments: establishments,
9635        rosganLotComitentes: (this.state.rosganLotComitentes || []).map(item => String(item.id) === String(this.rosganForm().comitenteId) && String(item.consignatariaId) === consignatariaId ? { ...item, operationComitenteId: resolvedComitenteId } : item),
9636        rosganLotPendingComitentes: (this.state.rosganLotPendingComitentes || []).map(item => String(item.id) === String(this.rosganForm().comitenteId) && String(item.consignatariaId) === consignatariaId ? { ...item, operationComitenteId: resolvedComitenteId } : item),
9637        rosganLotForm: { ...this.rosganForm(), establishmentId: selectedId },
9638        rosganComitenteSearch: '', rosganEstablishmentSearch: '', rosganEstablishmentMenuOpen: false,
9639        rosganLotRelationsScope: scope,
9640        rosganEstablishmentFormOpen: false, rosganEstablishmentSaving: false,
9641        rosganEstablishmentForm: this.emptyRosganEstablishmentForm(
9641), rosganEstablishmentMessage: null,
9642        rosganLotMessage: { ok: true, text: 'Establecimiento creado y seleccionado.' },
9643      });
9644      await this.loadRosganLotRelations({ preserve: true });
9645    } catch (error) {
9646      const detail = error?.data?.errors && typeof error.data.errors === 'object' ?Object.values(error.data.errors).flat().filter(Boolean)[0] : '';
9647      if (savedEstablishmentId) {
9648        this.setState({ rosganEstablishmentSaving:false, rosganEstablishmentFormOpen:false, rosganComitenteSearch:'', rosganLotMessage:{ ok:false, text:detail || error?.message || 'El establecimiento se creó, pero no se pudo actualizar la lista. No vuelvas a crearlo.' } });
9649        return;
9650      }
9651      this.setState({ rosganEstablishmentSaving: false, rosganEstablishmentMessage: { ok: false, text: detail || error?.message || 'No se pudo crear el establecimiento.' } });
9652    }
9653  }
9654  rosganSelectedCategory() {
9655    return (this.state.rosganLotCategories || []).find((item) => String(item.id) === String(this.rosganForm().categoryId)) || null;
9656  }
9657  rosganPositive(value) { const number = Number(value); return Number.isFinite(number) && number > 0 ?number : null; }
9658  rosganShowsConfinement() {
9659    const f = this.rosganForm();
9660    if (String(this.rosganSelectedCategory()?.formType).toLowerCase() === 'faena') return false;
9661    const option = this.rosganFieldOptions('nutritions', 'nutrition', 'nutricion').find((o) => String(o.value) === String(f.nutrition));
9662    return ['camporacion', 'camposupl', 'feedlot', 'racion', 'suplementados'].includes(this.consKey(option?.label || f.nutrition));
9663  }
9664  rosganDescriptionChoices() {
9665    const category = this.rosganSelectedCategory();
9666    if (String(category?.formType).toLowerCase() === 'faena') return [];
9667    const young = ['inv-terneros', 'inv-terneras', 'inv-terneros-as', 'inv-bubillos', 'inv-bubillas'].includes(category?.id);
9668    const cows = ['inv-vacas-inv', 'vientres-vacas-cria', 'vientres-vacas-cut-cria', 'vientres-vacas-gtia-prenez', 'vientres-vacas-cut-gtia-prenez'].includes(category?.id);
9669    const selected = this.rosganForm().breedIds || [];
9670    const hornedBreed = (this.state.rosganLotBreeds || []).filter(b => selected.includes(String(b.id))).some(b => /braford|hereford|brahman/.test(this.consKey(b.name || b.nombre)));
9671    return this.rosganFieldOptions('lotDescriptions', 'descriptionOptions', 'caracteristicas').filter(o => {
9672      const key = this.consKey(o.label);
9673      if (['alpiedelamadre', 'destetadosas'].includes(key)) return young;
9674      if (['mochosas', 'mochadosas', 'astadosas'].includes(key)) return hornedBreed;
9675      if (key === 'vacias') return cows;
9676      return false;
9677    });
9678  }
9679  rosganFaenaSubCategoryChoices() {
9680    return this.rosganFieldOptions('faenaSubCategories', 'subCategories', 'subCategory', 'subcategoria');
9681  }
9682  buildRosganLotPayload() {
9683    const f = this.rosganForm();
9684    const weighingKey = String(f.weighing || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z]/g, '');
9685    const weighing = ({ balanzapublica: 'Balanza Pública', enelcampo: 'en el Campo', balanzaferia: 'Balanza Feria' })[weighingKey] || null;
9686    const category = this.rosganSelectedCategory();
9687    const formType = String(category?.formType || '').toLowerCase();
9688    const choices = this.rosganDescriptionChoices();
9689    const selectedDescriptions = (Array.isArray(f.descriptionOptionValues) ?f.descriptionOptionValues : []).map((item) => choices.find((o) => [o.value,o.label].some(v => this.consKey(v) === this.consKey(item)))?.label).filter(Boolean);
9690    const lotDescriptions = [...new Set(selectedDescriptions)];
9691    const payload = {
9692      consignatariaId: f.consignatariaId, comitenteId: this.rosganSelectedOperationComitenteId(), establishmentId: f.establishmentId,
9693      categoryId: f.categoryId, breedIds: f.breedIds, ...window.RosganPaymentTerm.resolve(f),
9694      quantity: category?.quantityMode === 'total' ? this.rosganPositive(f.quantity) : null,
9695      maleQuantity: category?.quantityMode === 'male_female' && String(f.maleQuantity ?? '').trim() ? Number(f.maleQuantity) : null,
9696      femaleQuantity: category?.quantityMode === 'male_female' && String(f.femaleQuantity ?? '').trim() ? Number(f.femaleQuantity) : null,
9697      motherQuantity: category?.quantityMode === 'mothers_calves' ? this.rosganPositive(f.motherQuantity) : null,
9698      calfQuantity: category?.quantityMode === 'mothers_calves' ? this.rosganPositive(f.calfQuantity) : null,
9699      isTermino: !!category?.allowsTermino && ['inv-terneros', 'inv-terneras', 'inv-terneros-as'].includes(category?.id) && !!f.isTermino,
9700      isAptoChina: !!category?.allowsAptoChina && !!f.isAptoChina,
9701      lotDescriptions,
9702      observations: String(f.description || '').trim() || null,
9703      ageValue: category?.allowsAge ? this.rosganPositive(f.ageValue) : null, ageUnit: category?.allowsAge && this.rosganPositive(f.ageValue) ? f.ageUnit : null,
9704      dentition: category?.allowsDentition ? f.dentition || null : null,
9705      reproductiveStatus: category?.allowsReproductiveStatus ? f.reproductiveStatus || null : null,
9706      destination: category?.allowsDestination ? f.destination || null : null,
9707      bodyCondition: f.bodyCondition || null, animalClass: f.animalClass || null, hasMioMio: typeof f.hasMioMio === 'boolean' ? f.hasMioMio : null,
9708      tickStatus: f.tickStatus || null, health: f.health || null, nutrition: f.nutrition || null,
9709      daysOfConfinement: this.rosganShowsConfinement() ? this.rosganPositive(f.daysOfConfinement) : null,
9710      certifiedBy: String(f.certifiedBy || '').trim() || null,
9711    };
9712    if (formType === 'invernada') payload.invernada = {
9713      averageWeight: this.rosganPositive(f.averageWeight), maxWeight: this.rosganPositive(f.maxWeight) ?? undefined, minWeight: this.rosganPositive(f.minWeight) ?? undefined,
9714      weighing, desbaste: f.desbaste || null, hasLiquidBrand: !!f.hasLiquidBrand, isAptoVisec: !!f.isAptoVisec,
9715    };
9716    if (formType === 'vientres') payload.vientres = { service: f.service || null, pregnancy: f.pregnancy || null, calving: f.calving || null };
9717    if (formType === 'faena') payload.faena = {
9718      subCategory: f.subCategory || null, averageWeight: this.rosganPositive(f.averageWeight), maxWeight: this.rosganPositive(f.maxWeight) ?? undefined, minWeight: this.rosganPositive(f.minWeight) ?? undefined,
9719      conditionType: f.conditionType || null, deliveryType: f.deliveryType || null,
9720      weighing, finishing: f.finishing || null, estimatedYield: String(f.estimatedYield ?? '').trim() ? Number(f.estimatedYield) : undefined,
9721    };
9722    if (formType === 'faena') {
9723      ['bodyCondition', 'animalClass', 'hasMioMio', 'tickStatus', 'health', 'nutrition', 'daysOfConfinement'].forEach((key) => delete payload[key]);
9724    }
9725    return payload;
9726  }
9727  validateRosganLotDetails() {
9728    const f = this.rosganForm(); const category = this.rosganSelectedCategory();
9729    const type = String(category?.formType || '').toLowerCase();
9730    if (!category || category.isActive === false || !['invernada', 'vientres', 'faena'].includes(type)) return 'Seleccioná una categoría vigente.';
9731    if (this.state.rosganLotOptionsLoading || !this.state.rosganLotOptions) return 'Esperá a que se carguen las opciones del formulario e intentá nuevamente.';
9732    const quantities = category.quantityMode === 'male_female' ? [['maleQuantity', 'cantidad de machos'], ['femaleQuantity', 'cantidad de hembras']] : category.quantityMode === 'mothers_calves' ? [['motherQuantity', 'cantidad de madres'], ['calfQuantity', 'cantidad de crías']] : [['quantity', 'cantidad']];
9733    const integerFields = [...quantities];
9734    if (category.allowsAge) integerFields.push(['ageValue', 'edad']);
9735    if (type !== 'faena' && this.rosganShowsConfinement()) integerFields.push(['daysOfConfinement', 'días de encierre']);
9736    if (['invernada', 'faena'].includes(type)) integerFields.push(['averageWeight', 'peso promedio'], ['minWeight', 'peso mínimo'], ['maxWeight', 'peso máximo']);
9737    for (const [key, label] of integerFields) {
9738      if (String(f[key] ?? '').trim() && (!Number.isInteger(Number(f[key])) || Number(f[key]) < 0 || Number(f[key]) > 2147483647)) return 'El campo ' + label + ' debe ser un número entero válido, sin decimales.';
9739    }
9740    if (quantities.reduce((sum, [key]) => sum + (Number(f[key]) || 0), 0) > 2147483647) return 'La cantidad total supera el máximo permitido.';
9741    const required = [];
9742    if (category?.allowsAge && !this.rosganPositive(f.ageValue)) return 'Completá la edad con un valor mayor que cero.';
9743    if (type !== 'faena') {
9744      required.push(['tickStatus', 'garrapata'], ['bodyCondition', 'estado'], ['animalClass', 'clase'], ['health', 'sanidad'], ['nutrition', 'tratamiento nutricional']);
9745      if (typeof f.hasMioMio !== 'boolean') return 'Indicá Sí o No en Mío-mío.';
9746    }
9747    if (['invernada', 'faena'].includes(type)) {
9748      if (!this.rosganPositive(f.averageWeight)) return 'Completá el peso promedio con un valor mayor que cero.';
9749      if (String(f.minWeight ?? '').trim() && (!this.rosganPositive(f.minWeight) || Number(f.minWeight) > Number(f.averageWeight))) return 'El peso mínimo debe ser mayor que cero y no superar el peso promedio.';
9750      if (String(f.maxWeight ?? '').trim() && (!this.rosganPositive(f.maxWeight) || Number(f.maxWeight) < Number(f.averageWeight))) return 'El peso máximo debe ser mayor que cero y no ser menor al peso promedio.';
9751      required.push(['weighing', 'pesaje']);
9752    }
9753    if (type === 'vientres' && !(category.allowsReproductiveStatus && ['Vacías', 'Sin servicio'].includes(f.reproductiveStatus))) required.push(['service', 'servicio'], ['pregnancy', 'preñez'], ['calving', 'parición']);
9754    if (type === 'faena') required.push(['subCategory', 'subcategoría'], ['destination', 'destino'], ['conditionType', 'condición']);
9755    const missing = required.filter(([key]) => !String(f[key] ?? '').trim()).map(([, label]) => label);
9756    if (missing.length) return 'Completá los campos obligatorios: ' + missing.join(', ') + '.';
9757    const checks = [];
9758    if (category.allowsAge) checks.push(['ageUnit', 'unidad de edad', 'ageUnits']);
9759    if (category.allowsDentition) checks.push(['dentition', 'dentición', 'dentitions']);
9760    if (category.allowsReproductiveStatus) checks.push(['reproductiveStatus', 'estado reproductivo', type === 'vientres' ? 'vientresReproductiveStatuses' : 'invernadaReproductiveStatuses']);
9761    if (category.allowsDestination) checks.push(['destination', 'destino', type === 'faena' ? 'faenaDestinations' : 'generalDestinations']);
9762    if (type !== 'faena') checks.push(['bodyCondition', 'estado', 'bodyConditions'], ['animalClass', 'clase', 'classes'], ['tickStatus', 'garrapata', 'tickStatuses'], ['health', 'sanidad', 'healthStatuses'], ['nutrition', 'tratamiento nutricional', 'nutritions']);
9763    if (['invernada', 'faena'].includes(type)) checks.push(['weighing', 'pesaje', 'weighings']);
9764    if (type === 'faena') checks.push(['conditionType', 'condición', 'faenaConditionTypes'], ['deliveryType', 'entrega', 'faenaDeliveryTypes'], ['finishing', 'terminación', 'faenaFinishings']);
9765    for (const [key, label, catalog] of checks) {
9766      if (!String(f[key] ?? '').trim()) continue;
9767      if (!this.rosganFieldOptions(catalog).some(o => String(o.label).toLowerCase() === String(f[key]).toLowerCase())) return 'Seleccioná un valor vigente para ' + label + '.';
9768    }
9769    if (category.allowsAge && !String(f.ageUnit ?? '').trim()) return 'Seleccioná la unidad de edad.';
9770    if (type === 'faena') {
9771      if (!this.rosganFaenaSubCategoryChoices().some(o => o.label === f.subCategory)) return 'Seleccioná una subcategoría de faena válida.';
9772      if (String(f.estimatedYield ?? '').trim() && (!Number.isFinite(Number(f.estimatedYield)) || Number(f.estimatedYield) < 0 || Number(f.estimatedYield) > 100)) return 'El rendimiento estimado debe estar entre 0 y 100.';
9773    }
9774    return '';
9775  }
9776  validateRosganLot() {
9777    const f = this.rosganForm(); const category = this.rosganSelectedCategory();
9778    if ((f.paymentTermDays || []).includes('other') && !String(f.otherPaymentTermDay || '').trim()) return 'Indicá los días del plazo en Otro.';
9779    if (f.paymentTermReference === 'other' && !String(f.otherPaymentTermReference || '').trim()) return 'Indicá la referencia del plazo en Otro.';
9780    const payment = window.RosganPaymentTerm.resolve(f);
9781    if (payment.paymentTermReference && !payment.paymentTermDays.length) return 'Seleccioná un plazo de pago antes de indicar su referencia.';
9782    if (!this.canManageRosganLots(this.state.apiUser)) return 'Tu cuenta no tiene permiso para guardar lotes Rosgan.';
9783    if (!this.rosganConsignatariasForRemate(f.remateId).some((item) => String(item.id) === String(f.consignatariaId))) return 'La consignataria no está asignada a tu usuario o no participa en este remate.';
9784    if (f.weighing && ['invernada', 'faena'].includes(String(category?.formType).toLowerCase()) && !this.buildRosganLotPayload()[String(category.formType).toLowerCase()]?.weighing) return 'Seleccioná un pesaje válido: Balanza Pública, en el Campo o Balanza Feria.';
9785    if (!f.remateId || !f.consignatariaId || !f.comitenteId || !f.establishmentId || !f.categoryId) return 'Completá remate, consignataria, comitente, establecimiento y categoría.';
9786    const establishment = (this.state.rosganLotEstablishments || []).find(item => String(item.id) === String(f.establishmentId));
9787    if (!establishment || String(establishment.comitenteId) !== this.rosganSelectedOperationComitenteId()) return 'Seleccioná un establecimiento del alias elegido.';
9788    if (!Array.isArray(f.breedIds) || !f.breedIds.length) return 'Seleccioná al menos una raza.';
9789    if (f.breedIds.some(id => !(this.state.rosganLotBreeds || []).some(b => String(b.id) === String(id) && b.isActive !== false))) return 'Seleccioná razas vigentes del catálogo.';
9790    const breeds = [...new Set(f.breedIds)];
9791    const breedFamilies = this.rosganBreedFamilies().filter(group => group.key !== '__crosses__' && group.items.some(item => breeds.includes(String(item.id))));
9792    if (!breedFamilies.length) return 'Seleccioná al menos una raza.';
9793    if (breedFamilies.length > 3) return 'Podés seleccionar hasta 3 razas.';
9794    const dairy = ['holando-argentino', 'jersey', 'kiwicross'];
9795    if (breeds.some(id => dairy.includes(id)) && breeds.some(id => !dairy.includes(id))) return 'Holando Argentino, KiwiCross y Jersey no pueden combinarse con otras razas.';
9796    const mode = String(category?.quantityMode || 'total').toLowerCase();
9797    if (mode === 'male_female' && (!String(f.maleQuantity ?? '').trim() || !String(f.femaleQuantity ?? '').trim() || Number(f.maleQuantity) < 0 || Number(f.femaleQuantity) < 0 || Number(f.maleQuantity) + Number(f.femaleQuantity) <= 0)) return 'Completá machos y hembras con cero o más; la suma debe ser mayor que cero.';
9798    if (mode === 'mothers_calves' && (!this.rosganPositive(f.motherQuantity) || !this.rosganPositive(f.calfQuantity))) return 'Completá las cantidades de madres y crías.';
9799    if (!['male_female', 'mothers_calves'].includes(mode) && !this.rosganPositive(f.quantity)) return 'La cantidad debe ser mayor que cero.';
9800    return this.validateRosganLotDetails();
9801  }
9802  rosganLotStepComplete(step) {
9803    const f = this.rosganForm(); const category = this.rosganSelectedCategory();
9804    if (step === 0) { const farm = (this.state.rosganLotEstablishments || []).find(item => String(item.id) === String(f.establishmentId)); return !!(f.remateId && f.consignatariaId && f.comitenteId && farm && String(farm.comitenteId) === this.rosganSelectedOperationComitenteId()); }
9805    if (step === 1) {
9806      if (!f.categoryId || !f.breedIds.length) return false;
9807      const mode = String(category?.quantityMode || 'total').toLowerCase();
9808      if (mode === 'male_female') return String(f.maleQuantity ?? '').trim() !== '' && String(f.femaleQuantity ?? '').trim() !== '' && Number.isInteger(Number(f.maleQuantity)) && Number.isInteger(Number(f.femaleQuantity)) && Number(f.maleQuantity) >= 0 && Number(f.femaleQuantity) >= 0 && Number(f.maleQuantity) + Number(f.femaleQuantity) > 0;
9809      if (mode === 'mothers_calves') return !!(this.rosganPositive(f.motherQuantity) && this.rosganPositive(f.calfQuantity));
9810      return !!this.rosganPositive(f.quantity);
9811    }
9812    if (step === 2) return !!category && !this.validateRosganLotDetails();
9813    return true;
9814  }
9815  rosganLotNext() {
9816    const step = Number(this.state.rosganLotStep || 0);
9817    if (step < 2) {
9818      if (!this.rosganLotStepComplete(step)) {
9819        const messages = ['Completá remate, consignataria, comitente y establecimiento.', 'Elegí categoría, raza y cantidades.'];
9820        this.setState({ rosganLotMessage: { ok: false, text: messages[step] } });
9821        return;
9822      }
9823      this.setState({ rosganLotStep: step + 1, rosganLotMessage: null });
9824      window.scrollTo({ top: 0, behavior: 'smooth' });
9825      return;
9826    }
9827    if (step === 2) { this.publishRosganLot(); return; }
9828    if (step === 3) this.finishRosganLotLoad();
9829  }
9830  resetRosganLotForm() {
9831    const current = this.rosganForm();
9832    const keep = { remateId: current.remateId, consignatariaId: current.consignatariaId };
9833    this.setState({
9834      rosganLotStep: 0, rosganLotForm: { ...this.emptyRosganLotForm(), ...keep }, rosganCreatedLot: null, rosganEditingLot: null,
9835      rosganLotComitentes: keep.consignatariaId ?this.state.rosganLotComitentes : null,
9836      rosganLotEstablishments: keep.consignatariaId ?this.state.rosganLotEstablishments : null,
9837      rosganBreedSearch: '', rosganActiveBreedFamily: '', rosganBreedCrosses: {}, rosganLotMessage: null,
9838    });
9839  }
9840  rosganLotSaveError(error) {
9841    const errors = error?.data?.errors;
9842    const entries = errors && typeof errors === 'object' ? Object.entries(errors) : [];
9843    const fields = { averageWeight: 'peso promedio', minWeight: 'peso mínimo', maxWeight: 'peso máximo', estimatedYield: 'rendimiento estimado', ageValue: 'edad', quantity: 'cantidad', maleQuantity: 'cantidad de machos', femaleQuantity: 'cantidad de hembras', motherQuantity: 'cantidad de madres', calfQuantity: 'cantidad de crías', daysOfConfinement: 'días de encierre' };
9844    const specific = entries.filter(([key]) => key.toLowerCase() !== 'request');
9845    for (const [key, messages] of specific) {
9846      const field = key.split('.').pop();
9847      if (fields[field]) return 'Revisá el campo ' + fields[field] + ': la API no aceptó su valor.';
9848      const detail = (Array.isArray(messages) ? messages : [messages]).find(Boolean);
9849      if (detail) return String(detail);
9850    }
9851    if (entries.length) return 'La API no pudo interpretar los datos del lote. Revisá los campos e intentá nuevamente.';
9852    return error?.message || 'No se pudo guardar el lote Rosgan.';
9853  }
9854  async publishRosganLot() {
9855    if (this.state.rosganLotPublishing) return;
9856    const validation = this.validateRosganLot();
9857    if (validation) { this.setState({ rosganLotMessage: { ok: false, text: validation } }); return; }
9858    const f = this.rosganForm();
9859    const editing = this.state.rosganEditingLot;
9860    if (editing && (String(editing.remateId) !== String(f.remateId) || String(editing.consignatariaId) !== String(f.consignatariaId))) {
9861      this.setState({ rosganLotMessage: { ok: false, text: 'No se puede cambiar el remate ni la consignataria de un lote existente.' } });
9862      return;
9863    }
9864    const requestBody = JSON.stringify(this.buildRosganLotPayload());
9865    this.setState({ rosganLotPublishing: true, rosganLotMessage: null });
9866    try {
9867      const pendingFarm = (this.state.rosganLotEstablishments || []).find(item => String(item.id) === String(f.establishmentId));
9868      if (pendingFarm?.pendingComitenteId) await this.apiFetch('/api/legacy-comitentes', {method:'POST',body:JSON.stringify({action:'enable_for_lot',consignatariaId:f.consignatariaId,id:f.comitenteId,establishmentId:f.establishmentId})});
9869      const endpoint = '/api/remates/' + encodeURIComponent(f.remateId) + '/rosgan-lots' + (editing ?'/' + encodeURIComponent(editing.lotCert) : '');
9870      const created = await this.apiFetch(endpoint, { method: editing ?'PUT' : 'POST', body: requestBody });
9871      const cert = editing?.lotCert || created?.lotCert || created?.cert || created?.data?.lotCert || created?.data?.cert || created?.id || created?.data?.id || '';
9872      const createdLot = { remateId: f.remateId, lotCert: String(cert || ''), status: 'cargado', consignatariaId: f.consignatariaId, response: created };
9873      if (!cert) {
9874        this.setState({
9875          rosganLotStep: 3,
9876          rosganLotPublishing: false,
9877          rosganCreatedLot: createdLot,
9878          rosganLotMessage: {
9879            ok: false,
9880            text: 'El lote fue recibido, pero no pudimos completar su registro. No vuelvas a crearlo; contactá al equipo de auditoría para revisar el certificado.',
9881          },
9882        });
9883        return;
9884      }
9885      this.setState({ rosganLotStep: 3, rosganLotPublishing: false, rosganCreatedLot: createdLot, rosganEditingLot: null, rosganLotMessage: { ok: true, text: (editing ?'Lote actualizado' : 'Lote cargado. Quedó pendiente de control') + ' · Certificado ' + cert + '.' } }, () => {
9886        if (cert) this.fetchRosganLotVideos(f.remateId, cert, true);
9887      });
9888      window.scrollTo({ top: 0, behavior: 'smooth' });
9889    } catch (error) {
9890      this.setState({ rosganLotPublishing: false, rosganLotMessage: { ok: false, text: this.rosganLotSaveError(error) } });
9891    }
9892  }
9893  async openRosganLotEditor(lot = this.state.rdLightbox, remate = this.state.currentRemate, options = {}) {
9894    const duplicate = options.duplicate === true;
9895    if (!lot || (!duplicate && !this.canEditRosganLot(lot))) return;
9896    const remateId = this.remateRouteId(remate);
9897    const lotCert = this.remateLotRouteId(lot);
9898    if (!remateId || !lotCert) return;
9899    try {
9900      const response = await this.apiFetch(duplicate
9901        ? '/api/auditor/remates/' + encodeURIComponent(remateId) + '/lotes/' + encodeURIComponent(lotCert) + '/duplicate'
9902        : '/api/rosgan-lots/' + encodeURIComponent(lotCert) + '/internal');
9903      const source = { ...(response?.data?.lot || response?.lot || {}), ...(response?.data || response) };
9904      if ((!duplicate && !this.canEditRosganLot(source)) || String(source.consignatariaId) !== String(lot.consignatariaId)) throw new Error('El lote ya no está disponible para edición.');
9905      await this.loadRosganLotCatalogs();
9906      const category = (this.state.rosganLotCategories || []).find((item) => String(item.id) === String(source.categoryId));
9907      const form = {
9908        ...this.emptyRosganLotForm(),
9909        remateId: String(remateId), consignatariaId: String(source.consignatariaId),
9910        certifiedBy: source.certifiedBy || null, comitenteId: String(source.comitenteId || ''), establishmentId: String(source.establishmentId || ''),
9911        categoryId: String(source.categoryId || ''), formType: String(category?.formType || '').toLowerCase(),
9912        breedIds: (source.breedIds || source.breeds || []).map((entry) => String(entry?.id || entry?.breedId || entry)),
9913        ...window.RosganPaymentTerm.fromLot(source), quantity: source.quantity ?? '', maleQuantity: source.maleQuantity ?? '', femaleQuantity: source.femaleQuantity ?? '',
9914        motherQuantity: source.motherQuantity ?? '', calfQuantity: source.calfQuantity ?? '',
9915        isTermino: !!source.isTermino, isAptoChina: !!source.isAptoChina,
9916        description: source.observations || '', descriptionOptionValues: source.lotDescriptions || [],
9917        ageValue: source.ageValue ?? '', ageUnit: source.ageUnit || 'meses', dentition: source.dentition || '',
9918        reproductiveStatus: source.reproductiveStatus || '', destination: source.destination || '', bodyCondition: source.bodyCondition || '',
9919        animalClass: source.animalClass || '', hasMioMio: typeof source.hasMioMio === 'boolean' ? source.hasMioMio : null, tickStatus: source.tickStatus || '',
9920        health: source.health || '', nutrition: source.nutrition || '', daysOfConfinement: source.daysOfConfinement ?? '',
9921        averageWeight: source.invernada?.averageWeight ?? source.faena?.averageWeight ?? '',
9922        maxWeight: source.invernada?.maxWeight ?? source.faena?.maxWeight ?? '', minWeight: source.invernada?.minWeight ?? source.faena?.minWeight ?? '',
9923        weighing: source.invernada?.weighing || source.faena?.weighing || '', desbaste: source.invernada?.desbaste || '',
9924        hasLiquidBrand: !!source.invernada?.hasLiquidBrand, isAptoVisec: !!source.invernada?.isAptoVisec,
9925        service: source.vientres?.service || '', pregnancy: source.vientres?.pregnancy || '', calving: source.vientres?.calving || '',
9926        subCategory: source.faena?.subCategory || '', conditionType: source.faena?.conditionType || '',
9927        deliveryType: source.faena?.deliveryType || '', finishing: source.faena?.finishing || '', estimatedYield: source.faena?.estimatedYield ?? '',
9928      };
9929      if (duplicate) { this._auditorPanel?.close(); this._auditorPanel = null; }
9930      this.setState({ rosganComitenteSearch: null, rosganEstablishmentSearch: '', rosganCertifierMenuOpen: false });
9931      this.setState({ route: 'rosganLotLoad', rosganLotStep: 0, rosganLotForm: form, rosganCreatedLot: null, rosganEditingLot: duplicate ? null : { remateId, lotCert, consignatariaId: form.consignatariaId }, rosganLotMessage: duplicate ? { ok: true, text: 'Datos copiados del certificado ' + lotCert + '. Revisá y editá los datos; al guardar se creará un certificado nuevo en estado Cargado.' } : null }, () => this.loadRosganLotRelations());
9932      this.syncAppLocation('rosganLotLoad');
9933      window.scrollTo(0, 0);
9934    } catch (error) { this.showToast(error?.message || 'No se pudo abrir el lote para editar.'); }
9935  }
9936  finishRosganLotLoad() {
9937    const created = this.state.rosganCreatedLot;
9938    if (!created) return;
9939    const key = created.lotCert ?this.rosganLotVideoKey(created.remateId, created.lotCert) : '';
9940    const uploads = key ?(this.state.rosganLotVideoState?.[key]?.uploads || []) : [];
9941    if (key && this.state.rosganLotVideoState?.[key]?.uploading) return;
9942    const f = this.rosganForm();
9943    const keep = { remateId: f.remateId, consignatariaId: f.consignatariaId, comitenteId: f.comitenteId, establishmentId: f.establishmentId };
9944    this.setState({
9945      rosganLotStep: 0, rosganCreatedLot: null, rosganLotPublishing: false,
9946      rosganLotForm: { ...this.emptyRosganLotForm(), ...keep }, rosganBreedSearch: '', rosganActiveBreedFamily: '', rosganBreedCrosses: {},
9947      rosganLotMessage: { ok: true, text: 'Lote Rosgan cargado para control' + (created.lotCert ?' · Certificado ' + created.lotCert : '') + '.' },
9948    });
9949    window.scrollTo({ top: 0, behavior: 'smooth' });
9950  }
9951  INCREMENTS = [20, 50];
9952  CATALOGS = {
9953    categories: [['macho-entero-joven','Macho Entero Joven (MEJ)','male'],['novillitos','Novillitos (NT)','male'],['novillitos-vaquillonas','Novillitos (NT) y Vaquillonas (VQ)','mixed'],['novillos','Novillos (NO)','male'],['toros','Toros (TO)','male'],['vacas','Vacas (VA)','female'],['vaquillonas','Vaquillonas (VQ)','female']],
9954    breeds: [['aberdeen-angus-negro','Angus Negro'],['aberdeen-angus-colorado','Angus Colorado'],['aberdeen-angus-negro-colorado','Angus Negro y Colorado'],['brangus-negro','Brangus Negro'],['brangus-colorado','Brangus Colorado'],['hereford','Hereford'],['braford','Braford'],['charolais','Charolais'],['holando-argentino','Holando Argentino'],['shorthorn','Shorthorn'],['limousin','Limousin'],['limangus','Limangus'],['careta','Careta'],['media-sangre','Media Sangre'],['bubalinos','Bubalinos'],['bonsmara','Bonsmara'],['jersey','Jersey'],['brahman','Brahman'],['san-ignacio','San Ignacio'],['galloway','Galloway']],
9955    fatteningTypes: [['a-campo','A campo'],['racion','Ración'],['feedlot','Feedlot']],
9956    destinations: [['consumo','Consumo'],['exportacion-hilton','Export. Hilton'],['exportacion-ue','Export. UE'],['cuota-481','Cuota 481']],
9957    saleConditions: [['la-carne-a-levantar','La carne a levantar'],['kg-vivo-a-levantar','Kg vivo a levantar']],
9958    weighingOptions: [['balanza-publica','Balanza pública'],['en-el-campo','En el campo'],['balanza-feria','Balanza feria']],
9959    termOptions: [['contado','Contado'],['7-dias','7 días'],['14-dias','14 días'],['21-dias','21 días'],['30-dias','30 días'],['otro','Otro']],
9960    subcategories: [['especial','Especial',['novillos','novillitos','vaquillonas','mej']],['gordo-a','Gordo/a',['novillos','novillitos','vaquillonas','mej']],['regular','Regular',['vacas','toros']],['cons-buena','Cons. Buena',['vacas','toros']],['cons-inferior','Cons. Inferior',['vacas','toros']]],
9961  };
9962  WIZ_META = [
9963    { title: 'Categoría', text: 'Datos, razas y cantidad' },
9964    { title: 'Destino y pesos', text: 'Engorde, China y kilos' },
9965    { title: 'Comercial', text: 'Precio, plazo y carga' },
9966    { title: 'Videos', text: 'Material y portada' },
9967  ];
9968  emptyForm() { return { estId:'', estName:'', comitente:'', fecha:'', prov:'', loc:'', lat:'', lon:'', cat:'', subcat:'', cant:'', cantMachos:'', cantHembras:'', razas:[], engorde:'', diasEncierre:'', destino:'', china:'', pesoProm:'', pesoMin:'', pesoMax:'', cond:'', precio:'', listoParaCargar:false, dispo:'', desbaste:'', rinde:'', pesaje:'', plazo:'', plazoOtro:'', obs:'', videos:[null,null,null], videoPosters:[0,0,0], thumbIdx:0 }; }
9969  form() { return this.state.form || this.emptyForm(); }
9970  videoPoster(i) {
9971    const video = this.form().videos[i];
9972    return video?.selectedFrame?.previewUrl || video?.frames?.[0]?.previewUrl || '';
9973  }
9974  startVideoUpload(i) {
9975    if (this.state.vidProg[i] != null) return;
9976    this.openVideoPicker(i, false);
9977  }
9978  openVideoPicker(startIndex = 0, multiple = false) {
9979    const input = document.createElement('input');
9980    input.type = 'file';
9981    input.accept = 'video/*,.mp4,.mov,.webm,.m4v';
9982    input.multiple = multiple;
9983    input.style.display = 'none';
9984    input.addEventListener('change', async () => {
9985      const files = Array.from(input.files || []);
9986      input.remove();
9987      if (files.length) await this.handleVideoFiles(files, startIndex);
9988    }, { once: true });
9989    document.body.appendChild(input);
9990    input.click();
9991  }
9992  captureVideoFrame(video, time) {
9993    return new Promise((resolve) => {
9994      const canvas = document.createElement('canvas');
9995      const width = video.videoWidth || 1280;
9996      const height = video.videoHeight || 720;
9997      canvas.width = 640;
9998      canvas.height = Math.max(1, Math.round((height / width) * canvas.width));
9999      const handleSeeked = () => {
10000        canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
10001        canvas.toBlob((blob) => resolve(blob ?{ blob, previewUrl: URL.createObjectURL(blob) } : null), 'image/jpeg', 0.86);
10002      };
10003      video.addEventListener('seeked', handleSeeked, { once: true });
10004      video.currentTime = Math.min(Math.max(time, 0), Math.max(video.duration - 0.1, 0));
10005    });
10006  }
10007  extractVideoFrames(objectUrl) {
10008    return new Promise((resolve, reject) => {
10009      const video = document.createElement('video');
10010      video.preload = 'metadata';
10011      video.muted = true;
10012      video.playsInline = true;
10013      video.src = objectUrl;
10014      video.addEventListener('loadedmetadata', async () => {
10015        try {
10016          const duration = Number.isFinite(video.duration) && video.duration > 0 ?video.duration : 3;
10017          const frames = [];
10018          for (const time of [duration * 0.2, duration * 0.5, duration * 0.8]) {
10019            const frame = await this.captureVideoFrame(video, time);
10020            if (frame) frames.push(frame);
10021          }
10022          video.removeAttribute('src');
10023          video.load();
10024          resolve(frames);
10025        } catch (error) {
10026          reject(error);
10027        }
10028      }, { once: true });
10029      video.addEventListener('error', () => reject(new Error('No se pudieron leer los frames del video.')), { once: true });
10030    });
10031  }
10032  async handleVideoFile(i, file) {
10033    if (!file) return;
10034    const isVideo = file.type?.startsWith('video/') || /\.(mp4|mov|webm|m4v)$/i.test(file.name || '');
10035    if (!isVideo) throw new Error('Seleccioná un archivo de video MP4, MOV o WebM.');
10036    const previous = this.form().videos[i];
10037    const hadVideos = this.form().videos.some(Boolean);
10038    if (previous?.objectUrl) URL.revokeObjectURL(previous.objectUrl);
10039    (previous?.frames || []).forEach((frame) => frame?.previewUrl && URL.revokeObjectURL(frame.previewUrl));
10040    const objectUrl = URL.createObjectURL(file);
10041    const videos = [...this.form().videos];
10042    videos[i] = { file, fileName: file.name, contentType: file.type || 'application/octet-stream', fileSize: file.size, objectUrl, frames: [], selectedFrame: null };
10043    this.setState({
10044      form: { ...this.form(), videos, thumbIdx: hadVideos ?this.form().thumbIdx : i },
10045      vidProg: { ...this.state.vidProg, [i]: 12 },
10046      formMessage: { text: 'Generando portadas del video ' + (i + 1) + '...', ok: true },
10047    });
10048    try {
10049      const frames = await this.extractVideoFrames(objectUrl);
10050      if (!frames.length) throw new Error('No se pudieron generar portadas para el video.');
10051      const currentVideos = [...this.form().videos];
10052      currentVideos[i] = { ...currentVideos[i], frames, selectedFrame: frames[Math.floor(frames.length / 2)] || frames[0] };
10053      const prog = { ...this.state.vidProg }; delete prog[i];
10054      this.setState({ form: { ...this.form(), videos: currentVideos }, vidProg: prog, formMessage: null });
10055    } catch (error) {
10056      this.removeVideo(i);
10057      this.setState({ formMessage: { text: error.message || 'No se pudo procesar el video.', ok: false } });
10058    }
10059  }
10060  async handleVideoFiles(files, startIndex = 0) {
10061    const selected = files.filter((file) => file?.type?.startsWith('video/') || /\.(mp4|mov|webm|m4v)$/i.test(file?.name || '')).slice(0, 3);
10062    if (!selected.length) {
10063      this.setState({ formMessage: { text: 'Seleccioná archivos de video MP4, MOV o WebM.', ok: false } });
10064      return;
10065    }
10066    const videos = this.form().videos;
10067    const available = videos.map((video, index) => ({ video, index }))
10068      .filter(({ video, index }) => index >= startIndex && !video)
10069      .map(({ index }) => index);
10070    const targetSlots = available.length ?available : (startIndex < 3 ?[startIndex] : []);
10071    if (files.length > Math.min(targetSlots.length, 3)) {
10072      this.setState({ formMessage: { text: 'Solo se pueden cargar hasta 3 videos por lote.', ok: false } });
10073    }
10074    for (let index = 0; index < Math.min(selected.length, targetSlots.length, 3); index += 1) {
10075      await this.handleVideoFile(targetSlots[index], selected[index]);
10076    }
10077  }
10078  removeVideo(i) {
10079    const removed = this.form().videos[i];
10080    if (removed?.objectUrl) URL.revokeObjectURL(removed.objectUrl);
10081    (removed?.frames || []).forEach((frame) => frame?.previewUrl && URL.revokeObjectURL(frame.previewUrl));
10082    const prog = { ...this.state.vidProg }; delete prog[i];
10083    const nv = [...this.form().videos]; nv[i] = null;
10084    const thumbIdx = this.form().thumbIdx === i ?(nv.findIndex(Boolean) >= 0 ?nv.findIndex(Boolean) : 0) : this.form().thumbIdx;
10085    const deletedIds = removed?.existing && removed?.id
10086      ? Array.from(new Set([...(this.state.editDeletedVideoIds || []), String(removed.id)]))
10087      : (this.state.editDeletedVideoIds || []);
10088    this.setState({ vidProg: prog, form: { ...this.form(), videos: nv, thumbIdx }, editDeletedVideoIds: deletedIds, formMessage: null });
10089  }
10090  setVideoPoster(i, p) {
10091    const videos = [...this.form().videos];
10092    if (!videos[i]?.frames?.[p]) return;
10093    videos[i] = { ...videos[i], selectedFrame: videos[i].frames[p] };
10094    this.setF('videos', videos);
10095  }
10096  uploadAllVideos() { this.openVideoPicker(0, true); }
10097  filmAssistedVideo() { window.RosganFilmingAssistant?.open(file => this.handleVideoFiles([file], 0)); }
10098  setF(key, val, buffered = false) {
10099    const form = { ...this.form(), [key]: val };
10100    if (key === 'listoParaCargar' && val === true) form.dispo = '';
10101    if (key === 'engorde' && (val === 'racion' || val === 'feedlot') && form.destino === 'exportacion-hilton') {
10102      form.destino = '';
10103    }
10104    const patch = { form, formMessage: null };
10105    if (buffered) this.bufferInputState('lot-form-' + key, patch);
10106    else this.setState(patch);
10107  }
10108  loadDateOffset(isoDate, days) {
10109    const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(isoDate || ''));
10110    if (!match) return '';
10111    const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
10112    if (date.toISOString().slice(0, 10) !== isoDate) return '';
10113    date.setUTCDate(date.getUTCDate() + days);
10114    return date.toISOString().slice(0, 10);
10115  }
10116  loadAvailabilityError(fechaSubasta, listoParaCargar, fechaDisponibleCarga) {
10117    const min = this.loadDateOffset(fechaSubasta, 1);
10118    if (!min) return 'Elegí una fecha de subasta válida.';
10119    if (listoParaCargar === true) return '';
10120    const max = this.loadDateOffset(fechaSubasta, 7);
10121    if (!this.loadDateOffset(fechaDisponibleCarga, 0)) return 'Elegí la fecha disponible de carga.';
10122    if (fechaDisponibleCarga < min || fechaDisponibleCarga > max) return 'La carga debe estar disponible entre 1 y 7 días corridos después de la subasta.';
10123    return '';
10124  }
10125  toggleRaza(id) {
10126    const f = this.form(); const has = f.razas.includes(id);
10127    if (!has && f.razas.length >= 3) return;
10128    this.setF('razas', has ?f.razas.filter((r) => r !== id) : [...f.razas, id]);
10129  }
10130  catName(id) { return (this.CATALOGS.categories.find((c) => c[0] === id) || [null, ''])[1]; }
10131  catYieldPct(id) {
10132    const raw = (this.CATALOGS.categories.find((c) => String(c[0]) === String(id)) || [null, '', '', null])[3];
10133    const value = Number(raw);
10134    if (!Number.isFinite(value) || value <= 0) return '';
10135    return String(Math.round(value * 10000) / 100);
10136  }
10137  subcatsFor(catId) { return (this.CATALOGS.subcategories || []).filter((s) => !catId || (s[2] || []).includes(catId)); }
10138  subcatName(id) { return (this.CATALOGS.subcategories.find((s) => s[0] === id) || [null, ''])[1]; }
10139  async loadCatalog() {
10140    try {
10141      const d = await this.apiFetch('/api/Catalogs/lot-form');
10142      const srt = (a) => (a || []).slice().sort((x, y) => (x.sortOrder || 0) - (y.sortOrder || 0));
10143      const c = {};
10144      if (d.categories?.length) c.categories = srt(d.categories).map((x) => [x.id, x.name, x.sexType, x.meatYieldPercentage ?? null]);
10145      if (d.breeds?.length) c.breeds = srt(d.breeds).map((x) => [x.id, x.name]);
10146      if (d.feedingTypes?.length) c.fatteningTypes = srt(d.feedingTypes).map((x) => [x.id, x.name]);
10147      if (d.destinations?.length) c.destinations = srt(d.destinations).map((x) => [x.id, x.name]);
10148      if (d.conditions?.length) c.saleConditions = srt(d.conditions).map((x) => [x.id, x.name]);
10149      if (d.weighingTypes?.length) c.weighingOptions = srt(d.weighingTypes).map((x) => [x.id, x.name]);
10150      if (d.paymentTerms?.length) c.termOptions = srt(d.paymentTerms).map((x) => [x.id, x.name]);
10151      if (d.subcategories?.length) c.subcategories = srt(d.subcategories).map((x) => [x.id, x.name, x.categoryIds || []]);
10152      this.CATALOGS = { ...this.CATALOGS, ...c };
10153      this.forceUpdate();
10154    } catch (e) { console.warn('catalog', e); }
10155  }
10156  async loadLotYieldCategories(force = false, user = this.state.apiUser) {
10157    if (!this.isSysadmin(user) || this.state.lotYieldLoading) return;
10158    if (!force && Array.isArray(this.state.lotYieldCategories)) return;
10159    this.setState({ lotYieldLoading: true, lotYieldError: '', lotYieldMessage: null });
10160    try {
10161      const data = await this.apiFetch('/api/catalogs/lot-categories');
10162      const source = Array.isArray(data) ?data : Array.isArray(data?.data) ?data.data : Array.isArray(data?.items) ?data.items : [];
10163      const categories = source
10164        .slice()
10165        .sort((a, b) => (Number(a.sortOrder) || 0) - (Number(b.sortOrder) || 0))
10166        .map((category) => {
10167          const decimal = Number(category.meatYieldPercentage);
10168          return {
10169            ...category,
10170            uiPercent: Number.isFinite(decimal) && decimal >
10170 0 ?String(Math.round(decimal * 10000) / 100) : '',
10171          };
10172        });
10173      this.setState({ lotYieldCategories: categories, lotYieldLoading: false, lotYieldError: '' });
10174    } catch (error) {
10175      this.setState({ lotYieldLoading: false, lotYieldError: error?.data?.detail || error?.data?.message || error?.message || 'No pudimos cargar los rendimientos por categoría.' });
10176    }
10177  }
10178  setLotYieldPercent(categoryId, rawValue) {
10179    const normalized = String(rawValue ?? '').replace(',', '.').replace(/[^\d.]/g, '');
10180    const firstDot = normalized.indexOf('.');
10181    const value = firstDot < 0 ?normalized : normalized.slice(0, firstDot + 1) + normalized.slice(firstDot + 1).replace(/\./g, '');
10182    this.setState({
10183      lotYieldCategories: (this.state.lotYieldCategories || []).map((category) => String(category.id) === String(categoryId) ?{ ...category, uiPercent: value } : category),
10184      lotYieldMessage: null,
10185    });
10186  }
10187  async saveLotYieldCategory(categoryId) {
10188    if (!this.isSysadmin() || this.state.lotYieldSavingId) return;
10189    const current = (this.state.lotYieldCategories || []).find((category) => String(category.id) === String(categoryId));
10190    if (!current) return;
10191    const uiPercent = Number(String(current.uiPercent || '').replace(',', '.'));
10192    if (!Number.isFinite(uiPercent) || uiPercent < 1 || uiPercent > 100) {
10193      this.setState({ lotYieldMessage: { ok: false, text: 'Ingresá un rendimiento entre 1% y 100%.' } });
10194      return;
10195    }
10196    this.setState({ lotYieldSavingId: String(categoryId), lotYieldMessage: null });
10197    try {
10198      const response = await this.apiFetch('/api/catalogs/lot-categories/' + encodeURIComponent(categoryId) + '/meat-yield', {
10199        method: 'PATCH',
10200        body: JSON.stringify({ meatYieldPercentage: uiPercent / 100 }),
10201      });
10202      const updated = response?.category || response?.data?.category || { ...current, meatYieldPercentage: uiPercent / 100 };
10203      const decimal = Number(updated.meatYieldPercentage);
10204      const nextUiPercent = Number.isFinite(decimal) ?String(Math.round(decimal * 10000) / 100) : String(uiPercent);
10205      this.CATALOGS = {
10206        ...this.CATALOGS,
10207        categories: (this.CATALOGS.categories || []).map((item) => String(item[0]) === String(categoryId) ?[item[0], updated.name || item[1], updated.sexType || item[2], Number.isFinite(decimal) ?decimal : uiPercent / 100] : item),
10208      };
10209      this.setState({
10210        lotYieldCategories: (this.state.lotYieldCategories || []).map((category) => String(category.id) === String(categoryId) ?{ ...category, ...updated, uiPercent: nextUiPercent } : category),
10211        lotYieldSavingId: '',
10212        lotYieldMessage: { ok: true, text: response?.message || 'Rendimiento actualizado correctamente.' },
10213      });
10214    } catch (error) {
10215      this.setState({ lotYieldSavingId: '', lotYieldMessage: { ok: false, text: error?.data?.detail || error?.data?.message || error?.message || 'No pudimos guardar el rendimiento.' } });
10216    }
10217  }
10218  catSex(id) { return (this.CATALOGS.categories.find((c) => c[0] === id) || [null, '', ''])[2]; }
10219  isMixedCat(id) { return this.catSex(id) === 'mixed'; }
10220  labelOf(cat, id) { return (this.CATALOGS[cat].find((c) => c[0] === id) || [null, ''])[1]; }
10221  numOrNull(v) { const s = String(v || '').trim(); if (s === '') return null; const n = Number(s); return Number.isFinite(n) ?n : null; }
10222  clampPct(v, max) { const n = this.numOrNull(v); if (n === null) return null; return Math.max(0, Math.min(max, n)); }
10223  totalHeads(f) {
10224    if (this.isMixedCat(f.cat)) {
10225      const t = this.numOrNull(f.cant);
10226      if (t) return t;
10227      return (this.numOrNull(f.cantMachos) || 0) + (this.numOrNull(f.cantHembras) || 0);
10228    }
10229    return this.numOrNull(f.cant) || 0;
10230  }
10231  estComplete(f) {
10232    return !!f.estId;
10233  }
10234  loadStepComplete(i) {
10235    const f = this.form();
10236    if (i === 0) {
10237      const razasOk = f.razas.length >= 1 && f.razas.length <= 3;
10238      const cantOk = this.isMixedCat(f.cat)
10239        ? ((this.numOrNull(f.cantMachos) || 0) + (this.numOrNull(f.cantHembras) || 0)) > 0
10240        : (this.numOrNull(f.cant) || 0) > 0;
10241      return !!(this.estComplete(f) && f.cat && f.fecha && razasOk && cantOk);
10242    }
10243    if (i === 1) return !!(f.engorde && f.destino && f.china !== '' && this.numOrNull(f.pesoProm) !== null);
10244    if (i === 2) return !!(this.numOrNull(f.precio) !== null && f.cond && f.pesaje && f.plazo && (f.plazo !== 'otro' || String(f.plazoOtro || '').trim()) && !this.loadAvailabilityError(f.fecha, f.listoParaCargar, f.dispo));
10245    if (i === 3) return f.videos.some(Boolean);
10246    return false;
10247  }
10248  formCompleteness() {
10249    const f = this.form();
10250    const keys = ['estName', 'prov', 'loc', 'fecha', 'cat', 'engorde', 'destino', 'china', 'pesoProm', 'cond', 'precio', 'pesaje', 'plazo'];
10251    let done = keys.filter((k) => String(f[k] || '').length).length;
10252    if (this.estComplete(f)) done += 1;
10253    if (f.razas.length) done += 1;
10254    if (this.totalHeads(f) > 0) done += 1;
10255    if (f.videos.some(Boolean)) done += 1;
10256    return Math.round((done / (keys.length + 4)) * 100);
10257  }
10258  loadNext() {
10259    const step = this.state.loadStep;
10260    if (step < 3) {
10261      if (step === 2) {
10262        const f = this.form();
10263        const availabilityError = this.loadAvailabilityError(f.fecha, f.listoParaCargar, f.dispo);
10264        if (availabilityError) { this.setState({ formMessage: { text: availabilityError, ok: false } }); return; }
10265      }
10266      if (!this.loadStepComplete(step)) { this.setState({ formMessage: { text: 'Completá los campos obligatorios de esta etapa.', ok: false } }); return; }
10267      this.setState({ loadStep: step + 1, formMessage: null }); return;
10268    }
10269    this.publishLot();
10270  }
10271  buildLotPayload(f) {
10272    const mixed = this.isMixedCat(f.cat);
10273    const engordeCampo = f.engorde === 'a-campo';
10274    const p = {
10275      fechaSubasta: f.fecha,
10276      categoriaId: f.cat,
10277      subcategoriaId: f.subcat || f.subcategoriaId || '',
10278      razaIds: f.razas,
10279      cantidadTotal: mixed ?null : this.totalHeads(f),
10280      engordeId: f.engorde,
10281      destinoId: f.destino,
10282      aptoChina: f.china === 'true',
10283      pesoPromedio: this.numOrNull(f.pesoProm),
10284      pesoMinimo: this.numOrNull(f.pesoMin),
10285      pesoMaximo: this.numOrNull(f.pesoMax),
10286      listoParaCargar: f.listoParaCargar === true,
10287      fechaDisponibleCarga: f.listoParaCargar === true ?null : f.dispo,
10288      observaciones: f.obs,
10289      condicionId: f.cond,
10290      precioSugerido: this.numOrNull(f.precio),
10291      pesajeId: f.pesaje,
10292      desbaste: this.clampPct(f.desbaste, 100),
10293      rendimientoEstimado: this.clampPct(f.rinde, 100),
10294      plazoId: f.plazo,
10295      plazoOtro: f.plazo === 'otro' ?(String(f.plazoOtro || '').trim() || null) : null,
10296      establecimientoId: f.estId || null,
10297    };
10298    if (mixed) {
10299      p.cantidadMachos = this.numOrNull(f.cantMachos) || 0;
10300      p.cantidadHembras = this.numOrNull(f.cantHembras) || 0;
10301    }
10302    if (!engordeCampo) p.diasEncierre = this.numOrNull(f.diasEncierre) || 0;
10303    return p;
10304  }
10305  blobToDataUrl(blob) {
10306    return new Promise((resolve, reject) => {
10307      const reader = new FileReader();
10308      reader.onload = () => resolve(reader.result || '');
10309      reader.onerror = () => reject(new Error('No se pudo preparar la portada del video.'));
10310      reader.readAsDataURL(blob);
10311    });
10312  }
10313  createdLotId(data) {
10314    return data?.cert || data?.lot?.cert || data?.id || data?.lot?.id
10315      || data?.lotId || data?.lot?.lotId || data?.certificate || data?.lot?.certificate || '';
10316  }
10317  async uploadSignedFile(uploadUrl, file, onProgress) {
10318    try {
10319      await new Promise((resolve, reject) => {
10320        const xhr = new XMLHttpRequest();
10321        xhr.open('PUT', uploadUrl);
10322        xhr.withCredentials = false;
10323        xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
10324        xhr.upload.onprogress = (event) => {
10325          if (event.lengthComputable && onProgress) onProgress(Math.round((event.loaded / event.total) * 100));
10326        };
10327        xhr.onload = () => xhr.status >= 200 && xhr.status < 300
10328          ? resolve()
10329          : reject(new Error('No se pudo subir el archivo (HTTP ' + xhr.status + ').'));
10330        xhr.onerror = () => reject(Object.assign(new Error('No se pudo conectar con el almacenamiento de videos.'), { networkError: true }));
10331        xhr.send(file);
10332      });
10333    } catch (error) {
10334      if (!error?.networkError || !['localhost', '127.0.0.1'].includes(window.location.hostname)) throw error;
10335      await this.uploadSignedFileViaLocalProxy(uploadUrl, file, onProgress);
10336    }
10337  }
10338  async uploadSignedFileViaLocalProxy(uploadUrl, file, onProgress) {
10339    const token = await (await this.ensureFirebase()).auth().currentUser?.getIdToken();
10340    if (!token) throw new Error('La sesión venció. Volvé a ingresar antes de subir el video.');
10341    return new Promise((resolve, reject) => {
10342      const xhr = new XMLHttpRequest();
10343      xhr.open('POST', '/api/signed-upload');
10344      xhr.setRequestHeader('Authorization', 'Bearer ' + token);
10345      xhr.setRequestHeader('X-Upload-Url', uploadUrl);
10346      xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
10347      xhr.upload.onprogress = (event) => {
10348        if (event.lengthComputable && onProgress) onProgress(Math.round((event.loaded / event.total) * 100));
10349      };
10350      xhr.onload = () => {
10351        if (xhr.status >= 200 && xhr.status < 300) { resolve(); return; }
10352        let message = '';
10353        try { message = JSON.parse(xhr.responseText || '{}')?.message || ''; } catch {}
10354        reject(new Error(message || 'No se pudo subir el archivo mediante el servidor local (HTTP ' + xhr.status + ').'));
10355      };
10356      xhr.onerror = () => reject(new Error('Tampoco se pudo conectar con el servidor local de carga.'));
10357      xhr.send(file);
10358    });
10359  }
10360  async apiFetchWithFallback(primary, fallback, opts) {
10361    try {
10362      return await this.apiFetch(primary, opts);
10363    } catch (error) {
10364      if (![404, 405].includes(error?.status)) throw error;
10365      return this.apiFetch(fallback, opts);
10366    }
10367  }
10368  async uploadVideoAsset(lotId, video, slotIndex, completedSteps, totalSteps) {
10369    const uploadData = await this.apiFetch('/api/lots/' + encodeURIComponent(lotId) + '/videos/upload-url', {
10370      method: 'POST',
10371      body: JSON.stringify({
10372        fileName: video.fileName,
10373        contentType: video.file?.type || 'application/octet-stream',
10374        sizeBytes: video.fileSize,
10375      }),
10376    });
10377    const uploadUrl = uploadData?.uploadUrl || uploadData?.signedUrl;
10378    if (!uploadUrl) throw new Error('La API no devolvió una URL para subir el video ' + (slotIndex + 1) + '.');
10379    await this.uploadSignedFile(uploadUrl, video.file, (progress) => {
10380      this.setState({
10381        vidProg: { ...this.state.vidProg, [slotIndex]: progress },
10382        formMessage: { text: 'Subiendo video ' + (completedSteps + 1) + ' de ' + totalSteps + ' · ' + progress + '%', ok: true },
10383      });
10384    });
10385    const poster = video.selectedFrame?.blob
10386      ? await this.blobToDataUrl(video.selectedFrame.blob)
10387      : (video.selectedFrame?.previewUrl || '');
10388    await this.apiFetchWithFallback(
10389      '/api/lots/' + encodeURIComponent(lotId) + '/videos/metadata',
10390      '/api/lots/' + encodeURIComponent(lotId) + '/videos/complete',
10391      {
10392        method: 'POST',
10393        body: JSON.stringify({
10394          fileKey: uploadData.fileKey || uploadData.key,
10395          url: uploadData.publicUrl || uploadData.url,
10396          fileName: video.fileName,
10397          contentType: video.file?.type || 'application/octet-stream',
10398          sizeBytes: video.fileSize,
10399          thumbnailUrl: poster,
10400          posterUrl: poster,
10401        }),
10402      },
10403    );
10404    const progress = { ...this.state.vidProg };
10405    delete progress[slotIndex];
10406    this.setState({ vidProg: progress });
10407  }
10408  async uploadCoverAsset(lotId, selectedFrame, completedSteps, totalSteps) {
10409    if (!selectedFrame?.blob) return;
10410    const coverFile = new File([selectedFrame.blob], 'lot-' + lotId + '-cover.jpg', { type: 'image/jpeg' });
10411    const uploadData = await this.apiFetch('/api/lots/' + encodeURIComponent(lotId) + '/cover-image/upload-url', {
10412      method: 'POST',
10413      body: JSON.stringify({ fileName: coverFile.name, contentType: coverFile.type, sizeBytes: coverFile.size }),
10414    });
10415    const uploadUrl = uploadData?.uploadUrl || uploadData?.signedUrl;
10416    if (!uploadUrl) throw new Error('La API no devolvió una URL para subir la portada.');
10417    await this.uploadSignedFile(uploadUrl, coverFile, (progress) => {
10418      this.setState({ formMessage: { text: 'Subiendo portada ' + (completedSteps + 1) + ' de ' + totalSteps + ' · ' + progress + '%', ok: true } });
10419    });
10420    await this.apiFetchWithFallback(
10421      '/api/lots/' + encodeURIComponent(lotId) + '/cover-image/metadata',
10422      '/api/lots/' + encodeURIComponent(lotId) + '/cover-image/complete',
10423      {
10424        method: 'POST',
10425        body: JSON.stringify({
10426          fileKey: uploadData.fileKey || uploadData.key,
10427          url: uploadData.publicUrl || uploadData.url,
10428          fileName: coverFile.name,
10429          contentType: coverFile.type,
10430          sizeBytes: coverFile.size,
10431        }),
10432      },
10433    );
10434  }
10435  async rollbackCreatedLot(lotId) {
10436    if (!lotId) return false;
10437    try {
10438      await this.apiFetch('/api/lots/' + encodeURIComponent(lotId), { method: 'DELETE' });
10439      return true;
10440    } catch (error) {
10441      console.warn('No se pudo revertir el lote incompleto', lotId, error);
10442      return false;
10443    }
10444  }
10445  releaseVideoPreviews(videos) {
10446    (videos || []).forEach((video) => {
10447      if (video?.objectUrl) URL.revokeObjectURL(video.objectUrl);
10448      (video?.frames || []).forEach((frame) => frame?.previewUrl && URL.revokeObjectURL(frame.previewUrl));
10449    });
10450  }
10451  lotDetailToForm(lot, videoData) {
10452    const base = this.emptyForm();
10453    const breedIds = (lot.razaIds || lot.breedIds || lot.razas || [])
10454      .map((breed) => String(breed?.id || breed))
10455      .filter(Boolean)
10456      .slice(0, 3);
10457    const remoteVideos = (videoData || []).slice(0, 3).map((video) => {
10458      const url = video.url || video.publicUrl || video.downloadUrl || video.videoUrl || '';
10459      const poster = video.thumbnailUrl || video.posterUrl || video.thumbnail || lot.coverImage?.url || '';
10460      const frame = poster ?{ previewUrl: poster, remote: true } : null;
10461      return {
10462        id: String(video.id || video.videoId || video._id || video.fileKey || ''),
10463        existing: true,
10464        url,
10465        fileName: video.fileName || video.name || 'Video publicado',
10466        contentType: video.contentType || 'video/mp4',
10467        fileSize: Number(video.sizeBytes || video.fileSize || 0),
10468        frames: frame ?[frame] : [],
10469        selectedFrame: frame,
10470      };
10471    });
10472    while (remoteVideos.length < 3) remoteVideos.push(null);
10473    const coverUrl = lot.coverImage?.url || lot.thumbnailUrl || '';
10474    const coverIndex = Math.max(0, remoteVideos.findIndex((video) =>
10475      video && [video.selectedFrame?.previewUrl, video.url].includes(coverUrl)));
10476    return {
10477      ...base,
10478      estId: String(lot.establecimientoId || ''),
10479      estName: lot.establecimientoNombre || '',
10480      comitente: lot.establecimientoComitente || '',
10481      fecha: String(lot.fechaSubasta || '').slice(0, 10),
10482      prov: lot.establecimientoProvincia || lot.provincia || '',
10483      loc: lot.establecimientoLocalidad || lot.localidad || '',
10484      lat: lot.establecimientoLatitud != null ?String(lot.establecimientoLatitud) : '',
10485      lon: lot.establecimientoLongitud != null ?String(lot.establecimientoLongitud) : '',
10486      cat: lot.categoriaId || '',
10487      subcat: lot.subcategoriaId || '',
10488      cant: lot.cantidadTotal != null ?String(lot.cantidadTotal) : '',
10489      cantMachos: lot.cantidadMachos != null ?String(lot.cantidadMachos) : '',
10490      cantHembras: lot.cantidadHembras != null ?String(lot.cantidadHembras) : '',
10491      razas: breedIds,
10492      engorde: lot.engordeId || '',
10493      diasEncierre: lot.diasEncierre != null ?String(lot.diasEncierre) : '',
10494      destino: lot.destinoId || '',
10495      china: lot.aptoChina === true || lot.aptoChina === 'true' ?'true' : 'false',
10496      pesoProm: lot.pesoPromedio != null ?String(lot.pesoPromedio) : '',
10497      pesoMin: lot.pesoMinimo != null ?String(lot.pesoMinimo) : '',
10498      pesoMax: lot.pesoMaximo != null ?String(lot.pesoMaximo) : '',
10499      cond: lot.condicionId || '',
10500      precio: lot.precioSugerido != null ?String(lot.precioSugerido) : '',
10501      listoParaCargar: lot.listoParaCargar === true,
10502      dispo: String(lot.fechaDisponibleCarga || '').slice(0, 10),
10503      desbaste: lot.desbaste != null ?String(lot.desbaste) : '',
10504      rinde: lot.rendimientoEstimado != null ?String(lot.rendimientoEstimado) : '',
10505      pesaje: lot.pesajeId || '',
10506      plazo: lot.plazoId || '',
10507      plazoOtro: lot.plazoOtro || '',
10508      obs: lot.observaciones || '',
10509      videos: remoteVideos,
10510      thumbIdx: coverIndex,
10511    };
10512  }
10513  async startEditLot(mappedLot) {
10514    if (!mappedLot || ['sold', 'closed_no_bids'].includes(mappedLot.auctionStatus) || mappedLot.status === 'Vendido' || mappedLot.status === 'Caducado') return;
10515    const cert = String(mappedLot.cert || '');
10516    if (!cert || this.state.editLotLoading) return;
10517    this.setState({ editLotLoading: true });
10518    this.showToast('Cargando datos del lote...');
10519    try {
10520      const [detailData, videosData] = await Promise.all([
10521        this.apiFetch('/api/lots/' + encodeURIComponent(cert)),
10522        this.apiFetch('/api/lots/' + encodeURIComponent(cert) + '/videos').cat
10522ch(() => []),
10523      ]);
10524      const detail = detailData?.lot || detailData?.data || detailData || mappedLot.raw || mappedLot;
10525      const videos = Array.isArray(videosData)
10526         ? videosData
10527        : (videosData?.items || videosData?.videos || videosData?.data || []);
10528      const resolvedVideos = Array.isArray(videos) && videos.length
10529         ? videos
10530        : (detail.videos || detail.videoUploads || []);
10531      const editForm = this.lotDetailToForm(detail, Array.isArray(resolvedVideos) ?resolvedVideos : []);
10532      this.setState({
10533        route: 'load',
10534        loadStep: 0,
10535        form: editForm,
10536        formMessage: null,
10537        editLotCert: cert,
10538        editOriginalForm: editForm,
10539        editDeletedVideoIds: [],
10540        editLotLoading: false,
10541        vidProg: {},
10542      });
10543      window.scrollTo(0, 0);
10544    } catch (error) {
10545      this.setState({ editLotLoading: false });
10546      this.showToast(error.message || 'No se pudo abrir el lote para editar.');
10547    }
10548  }
10549  async openLotDestination() {
10550    this.setState({lotDestinationOpen:true,lotDestinationLoading:true,lotDestinationError:'',lotDestinationItems:[]});
10551    try {
10552      const uid=this.state.apiUser?.uid || this.state.user?.uid;
10553      const [raw]=await Promise.all([this.apiFetch('/api/Remates'),uid&&!this.canManageUsers()?this.fetchUserConsignataria(uid):Promise.resolve()]);
10554      this.setState({lotDestinationItems:this.rosganArray(raw),lotDestinationLoading:false});
10555    } catch(error) {this.setState({lotDestinationLoading:false,lotDestinationError:error.message||'No se pudieron consultar tus remates.'});}
10556  }
10557  lotDestinationLogos(item) {
10558    const all=[...(this.state.rosganLotConsignatarias||[]),...(this.state.consignatarias||[])];
10559    const participants=item.consignatariasParticipantes?.length?item.consignatariasParticipantes:[{consignatariaId:item.consignatariaId,consignatariaNombre:item.consignatariaNombre||item.nombre_consignatario||'Rosgan'}];
10560    const seen=new Set();
10561    return participants.map(participant=>{
10562      const ref=participant.consignatariaId||participant.id||participant.consignatariaNombre||'Rosgan';
10563      const found=all.find(c=>[c.id,c.slug,c.name,c.nombre].some(v=>v&&this.consKey(v)===this.consKey(ref)));
10564      const name=found?.name||found?.nombre||participant.consignatariaNombre||participant.name||ref;
10565      const white=this.consWhiteLogo(ref);return {name,logo:white||this.consLogo(ref)||'/assets/brand/rosgan-iso-blue.png',color:white?this.consBrandColor(ref):'#fff'};
10566    }).filter(brand=>{if(seen.has(brand.name))return false;seen.add(brand.name);return true;});
10567  }
10568  lotDestinationAllowed(item) {
10569    if(!this.canManageRosganLots() || item.activo===false || item.cargaDeLotes===false || item.closeRemate===true || item.load?.lotes===false || String(item.cargaLotes||'').toLowerCase()==='cerrada')return false;
10570    if(item.cabanasConfig && !this.canManageSellerData())return false;
10571    if(this.canManageUsers())return true;
10572    if(item.cabanasConfig)return this.canEditCabanaRemate(item);
10573    const memberships=[...(this.state.userConsignatarias||[]),this.state.userConsignataria].filter(Boolean);
10574    const keys=new Set(memberships.flatMap(c=>[c.id,c.slug,c.ref,c.name]).filter(Boolean).map(v=>this.consKey(v)));
10575    const participants=item.consignatariasParticipantes||[];
10576    return participants.some(c=>[c.consignatariaId,c.id,c.consignatariaNombre,c.name].some(v=>v&&keys.has(this.consKey(v)))) || [item.consignatariaId,item.consignatariaNombre].some(v=>v&&keys.has(this.consKey(v)));
10577  }
10578  async chooseLotDestination(item) {
10579    if(!this.lotDestinationAllowed(item))return;
10580    this.setState({lotDestinationOpen:false});
10581    if(item.cabanasConfig){try {const remate=await this.fetchCabanaById(String(item.id),{force:true});if(remate)this.openCabanaLotCreate(remate);}catch(error){this.showToast(error.message||'No se pudo abrir el remate.');}return;}
10582    await this.loadRosganLotCatalogs();
10583    this.onRosganRemateChange(String(item.id));
10584    this.go('rosganLotLoad');
10585  }
10586  beginNewLot() {
10587    if (!this.canAccessRoslot()) return;
10588    this.releaseVideoPreviews(this.form().videos);
10589    this.setState({
10590      route: 'load',
10591      loadStep: 0,
10592      form: this.emptyForm(),
10593      formMessage: null,
10594      editLotCert: '',
10595      editOriginalForm: null,
10596      editDeletedVideoIds: [],
10597      vidProg: {},
10598    });
10599    window.scrollTo(0, 0);
10600  }
10601  resetLotWizard() {
10602    this.releaseVideoPreviews(this.form().videos);
10603    this.setState({ form: this.state.editLotCert && this.state.editOriginalForm ?this.state.editOriginalForm : this.emptyForm(), loadStep: 0, formMessage: null, editDeletedVideoIds: [], vidProg: {} });
10604  }
10605  closeLotWizard() {
10606    this.releaseVideoPreviews(this.form().videos);
10607    this.setState({ route: this.state.editLotCert ?'profile' : 'pool', form: this.emptyForm(), loadStep: 0, formMessage: null, editLotCert: '', editOriginalForm: null, editDeletedVideoIds: [], vidProg: {} });
10608    window.scrollTo(0, 0);
10609  }
10610  async deleteEditedVideos(lotId) {
10611    for (const videoId of (this.state.editDeletedVideoIds || [])) {
10612      await this.apiFetch('/api/lots/' + encodeURIComponent(lotId) + '/videos/' + encodeURIComponent(videoId), { method: 'DELETE' });
10613    }
10614  }
10615  async publishLot() {
10616    if (this.state.publishing) return;
10617    const f = this.form();
10618    const availabilityError = this.loadAvailabilityError(f.fecha, f.listoParaCargar, f.dispo);
10619    if (availabilityError) { this.setState({ loadStep: 2, formMessage: { text: availabilityError, ok: false } }); return; }
10620    for (let i = 0; i <= 3; i++) {
10621      if (!this.loadStepComplete(i)) { this.setState({ loadStep: i, formMessage: { text: 'Faltan campos obligatorios en la etapa ' + (i + 1) + '.', ok: false } }); return; }
10622    }
10623    this.setState({ publishing: true, formMessage: { text: 'Publicando lote...', ok: true } });
10624    let token = '';
10625    try { token = await (await this.ensureFirebase()).auth().currentUser?.getIdToken(); } catch {}
10626    if (!token) {
10627      this.setState({ publishing: false, formMessage: { text: 'Tenés que iniciar sesión para publicar el lote.', ok: false } });
10628      return;
10629    }
10630    const payload = this.buildLotPayload(f);
10631    const videoUploads = f.videos.filter((video) => video?.file);
10632    const coverVideo = f.videos[f.thumbIdx] || videoUploads[0];
10633    const selectedCover = coverVideo?.selectedFrame || coverVideo?.frames?.[0] || null;
10634    const totalUploadSteps = videoUploads.length + (selectedCover ?1 : 0);
10635    let createdLotId = '';
10636    let createdLotNeedsRollback = false;
10637    try {
10638      const editing = !!this.state.editLotCert;
10639      const created = await this.apiFetch(
10640        editing ?'/api/Lots/' + encodeURIComponent(this.state.editLotCert) : '/api/Lots',
10641        { method: editing ?'PUT' : 'POST', body: JSON.stringify(payload) },
10642      );
10643      createdLotId = editing ?this.state.editLotCert : this.createdLotId(created);
10644      if (!createdLotId && totalUploadSteps) {
10645        throw new Error('El lote se creó, pero la API no devolvió el certificado para subir los videos.');
10646      }
10647      createdLotNeedsRollback = !editing && totalUploadSteps > 0;
10648      for (let index = 0; index < videoUploads.length; index += 1) {
10649        const slotIndex = f.videos.indexOf(videoUploads[index]);
10650        await this.uploadVideoAsset(createdLotId, videoUploads[index], slotIndex, index, totalUploadSteps);
10651      }
10652      if (selectedCover) {
10653        await this.uploadCoverAsset(createdLotId, selectedCover, videoUploads.length, totalUploadSteps);
10654      }
10655      if (editing && this.state.editDeletedVideoIds.length) {
10656        this.setState({ formMessage: { text: 'Actualizando videos del lote...', ok: true } });
10657        await this.deleteEditedVideos(createdLotId);
10658      }
10659      createdLotNeedsRollback = false;
10660      this.releaseVideoPreviews(f.videos);
10661      this.setState({ publishing: false, formMessage: { text: editing ?'Lote actualizado correctamente.' : 'Lote publicado. Ya aparece en el pool.', ok: true }, editDeletedVideoIds: [] });
10662      this.fetchLots();
10663      this.fetchMyLots();
10664      setTimeout(() => this.setState({ route: editing ?'profile' : 'pool', form: this.emptyForm(), loadStep: 0, formMessage: null, editLotCert: '', editOriginalForm: null }), 1400);
10665    } catch (e) {
10666      let rollbackText = '';
10667      if (createdLotNeedsRollback && createdLotId) {
10668        const reverted = await this.rollbackCreatedLot(createdLotId);
10669        rollbackText = reverted
10670           ? ' El lote incompleto fue eliminado.'
10671          : ' Revisá Mis lotes: no se pudo eliminar automáticamente la publicación incompleta.';
10672      }
10673      this.setState({
10674        publishing: false,
10675        vidProg: {},
10676        formMessage: { text: (e.message || 'No se pudo publicar. Verificá tu sesión e intentá de nuevo.') + rollbackText, ok: false },
10677      });
10678    }
10679  }
10680  syncCatalogFloatingContact() {
10681    const floating = document.querySelector('.dc-catalog-floating-contact');
10682    if (!floating) return;
10683    const card = document.querySelector('.dc-catalog-contact-row .dc-lot-contact-card');
10684    const shouldShow = this.state.route === 'cabanaDetail' && this.state.isMobile && !!card && card.getBoundingClientRect().bottom < 74;
10685    floating.classList.toggle('is-visible', shouldShow);
10686    floating.setAttribute('aria-hidden', shouldShow ? 'false' : 'true');
10687  }
10688  lacassinaLocationState() {
10689    const path = window.location.pathname.replace(/\/+$/, '') || '/';
10690    const isStats = path === '/lacassina/estadisticas';
10691    const isCommercial = path === '/lacassina/comercial';
10692    const isPrices = path === '/lacassina/precios';
10693    const isTv = path === '/lacassina/tv';
10694    const isTvConfig = path === '/lacassina/tv/config';
10695    const isStandalone = isStats || isCommercial || isPrices || isTv || isTvConfig;
10696    const lotMatch = isStandalone ?null : path.match(/^\/lacassina\/([^/]+)\/([^/]+)$/i);
10697    return {
10698      path: lotMatch ?'/lacassina' : path,
10699      route: isStats ?'lacassina-stats' : (isCommercial ?'lacassina-commercial' : (isPrices ?'lacassina-prices' : (isTv ?'lacassina-tv' : (isTvConfig ?'lacassina-tv-config' : 'lacassina')))),
10700      lotNumber: lotMatch ?decodeURIComponent(lotMatch[1]) : '',
10701      rp: lotMatch ?decodeURIComponent(lotMatch[2]) : '',
10702      isLot: !!lotMatch,
10703      isStats,
10704      isCommercial,
10705      isPrices,
10706      isTv,
10707      isTvConfig,
10708    };
10709  }
10710  setLacassinaPublicLocation(info) {
10711    window.__ROSGAN_PUBLIC_PATH__ = info.path;
10712    window.__ROSGAN_PUBLIC_ROUTE__ = info.route;
10713    window.__ROSGAN_PUBLIC_LOT_NUMBER__ = info.lotNumber || '';
10714    window.__ROSGAN_PUBLIC_RP__ = info.rp || '';
10715    document.documentElement.dataset.publicRoute = info.route;
10716  }
10717  handleLacassinaPopState() {
10718    if (this.state.publicAdvanceMode !== 'lacassina') return;
10719    const info = this.lacassinaLocationState();
10720    this.setLacassinaPublicLocation(info);
10721    this.flushCabanaLotViewTimer('browser_navigation');
10722    if (info.isStats) {
10723      this.setState({ route: 'cabanaStats', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [] });
10724      return;
10725    }
10726    if (info.isCommercial) {
10727      this.setState({ route: 'cabanaCommercial', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cabanaCommercialLoading: true }, () => {
10728        window.scrollTo(0, 0);
10729        if (this.state.currentCabana) this.fetchCabanaCommercial(this.state.currentCabana);
10730      });
10731      return;
10732    }
10733    if (info.isPrices) {
10734      this.setState({ route: 'cabanaPriceBoard', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cabanaCommercialDrafts: this.loadCabanaCommercialDrafts(this.state.currentCabana) }, () => {
10735        window.scrollTo(0, 0);
10736        if (this.state.currentCabana) this.fetchCabanaCommercial(this.state.currentCabana, { silent: true });
10737      });
10738      return;
10739    }
10740    if (info.isTv || info.isTvConfig) {
10741      this.setState({ route: info.isTvConfig ?'cabanaTvConfig' : 'cabanaTvLower', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cabanaLotsLoading: true }, () => {
10742        window.scrollTo(0, 0);
10743        if (this.state.currentCabana) this.fetchCabanaLots(this.state.currentCabana);
10744      });
10745      return;
10746    }
10747    if (!info.isLot) {
10748      this.setState({ route: 'cabanaDetail', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cbLotVideo: 0, c
10748abanaLotVideosLoading: false, cabanaLotMediaReady: false, cabanaLotMediaFailed: false }, () => window.scrollTo(0, 0));
10749      return;
10750    }
10751    const openTarget = (lots) => {
10752      const opened = this.openCabanaLotByRp(info.rp, lots, info.lotNumber);
10753      if (!opened) this.setState({ route: 'cabanaDetail', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [] });
10754    };
10755    this.setState({ cabanaPublicDeepLoading: true }, () => {
10756      if (Array.isArray(this.state.cabanaLots) && this.state.cabanaLots.length) {
10757        openTarget(this.state.cabanaLots);
10758      } else if (this.state.currentCabana) {
10759        this.fetchCabanaLots(this.state.currentCabana).then(openTarget).catch(() => this.setState({ cabanaPublicDeepLoading: false }));
10760      } else {
10761        this.fetchCabanaById('2', { force: true }).then((remate) => {
10762          if (!remate) throw new Error('No se encontro el remate de La Cassina.');
10763          this.openCabana(remate, { focusRp: info.rp, focusLotNumber: info.lotNumber });
10764        }).catch(() => this.setState({ cabanaPublicDeepLoading: false }));
10765      }
10766    });
10767  }
10768  phoneNumberE164(value) {
10769    let phone = String(value || '').trim().replace(/[\s().-]/g, '');
10770    if (phone.startsWith('00')) phone = '+' + phone.slice(2);
10771    return /^\+[1-9]\d{7,14}$/.test(phone) ?phone : '';
10772  }
10773  maskedPhoneNumber(value) {
10774    const phone = String(value || '');
10775    if (phone.length < 7) return phone;
10776    return phone.slice(0, 4) + ' •••• ' + phone.slice(-4);
10777  }
10778  clearPhoneRecaptcha() {
10779    try { this._phoneRecaptchaVerifier?.clear?.(); } catch {}
10780    this._phoneRecaptchaVerifier = null;
10781    this._phoneRecaptchaWidgetId = null;
10782  }
10783  async savePhoneAndContinue() {
10784    if (this.state.phoneVerificationBusy) return;
10785    const phoneNumber = this.phoneNumberE164(this.state.phoneVerificationNumber);
10786    if (!phoneNumber) { this.setState({ phoneVerificationMessage: 'Ingresá tu teléfono con código de país, por ejemplo +54 9 341 000 0000.' }); return; }
10787    this.setState({ phoneVerificationBusy: true, phoneVerificationMessage: '' });
10788    try {
10789      const user = (await this.ensureFirebase()).auth().currentUser;
10790      if (!user) throw new Error('La sesión venció. Volvé a ingresar.');
10791      const token = await user.getIdToken();
10792      const response = await fetch('/api/auth/sync-user', {
10793        method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
10794        body: JSON.stringify({ phoneNumber }),
10795      });
10796      const result = await response.json();
10797      if (!response.ok || !this.phoneNumberE164(result?.session?.phoneNumber)) throw new Error(result?.message || 'No se pudo guardar el teléfono. Intentá nuevamente.');
10798      if ((await this.ensureFirebase()).auth().currentUser?.uid !== user.uid) throw new Error('La sesión cambió. Volvé a ingresar.');
10799      const access = await this.authVerificationAccess(user);
10800      await this.activateAuthenticatedUser(user, access.apiUser);
10801      this.setState({ phoneVerificationRequired: false, phoneVerificationBusy: false, phoneVerificationMessage: '' });
10802    } catch (error) {
10803      this.setState({ phoneVerificationBusy: false, phoneVerificationMessage: error?.message || 'No se pudo guardar el teléfono.' });
10804    }
10805  }
10806  changePhoneNumber() {
10807    if (this.state.phoneVerificationBusy) return;
10808    this._phoneConfirmationResult = null;
10809    this._phoneVerificationId = null;
10810    this.clearPhoneRecaptcha();
10811    this.setState({ phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationMessage: '' });
10812  }
10813  async logoutFromPhoneVerification() {
10814    if (this.state.phoneVerificationBusy) return;
10815    this.clearWelcomeAfterSignIn();
10816    this.clearCachedApiUser();
10817    this._phoneConfirmationResult = null;
10818    this._phoneVerificationId = null;
10819    this.clearPhoneRecaptcha();
10820    try { sessionStorage.removeItem('rosgan:lastRoute'); } catch {}
10821    await this.signOutSession();
10822    this.setState({ user: null, apiUser: null, routeLoading: false, userConsignataria: null, userConsignatarias: [], myLots: null, myBids: null, profileFetchInfo: {}, route: 'login', authReady: true, authMode: 'signin', phoneVerificationRequired: false, phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationBusy: false, phoneVerificationMessage: '' });
10823  }
10824  isPasswordAccount(user) {
10825    return !!user && Array.isArray(user.providerData) && user.providerData.some((provider) => provider?.providerId === 'password');
10826  }
10827  requiresEmailVerification(user) {
10828    return false;
10829  }
10830  authActionSettings(action) {
10831    const target = new URL(window.location.origin + '/');
10832    target.searchParams.set('authAction', action);
10833    return { url: target.toString(), handleCodeInApp: false };
10834  }
10835  async sendRegistrationVerification(user) {
10836    if (!user || !this.requiresEmailVerification(user)) return;
10837    const response = await this.authOperationWithTimeout(
10838      this.apiFetch('/api/mailing/auth/verification-email', { method: 'POST', body: '{}' }),
10839      30000,
10840      'El servicio de correo no confirmó el envío. Tocá “Reenviar correo” para intentarlo nuevamente.'
10841    );
10842    const result = response?.data || response;
10843    if (result?.alreadyVerified === true) return result;
10844    if (result?.accepted !== true || result?.provider !== 'mailgun') {
10845      throw new Error('El servicio de correo no confirmó el envío. Tocá “Reenviar correo” para intentarlo nuevamente.');
10846    }
10847    return result;
10848  }
10849  authOperationWithTimeout(promise, timeoutMs, message) {
10850    let timer = null;
10851    return Promise.race([
10852      Promise.resolve(promise).finally(() => { if (timer) clearTimeout(timer); }),
10853      new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); }),
10854    ]);
10855  }
10856  async checkEmailVerification(options = {}) {
10857    if (this.state.emailVerificationBusy) return;
10858    const silent = options?.silent === true;
10859    this.setState({ emailVerificationBusy: true, emailVerificationMessage: silent ?this.state.emailVerificationMessage : '' });
10860    try {
10861      const firebase = await this.ensureFirebase();
10862      const user = firebase.auth().currentUser;
10863      if (!user) throw new Error('La sesión venció. Volvé a ingresar.');
10864      await this.authOperationWithTimeout(user.reload(), 8000, 'Firebase demoró demasiado en comprobar el correo. Probá nuevamente.');
10865      if (sessionEpoch !== (this._sessionEpoch || 0) || !firebase.auth().currentUser) return;
10866    const currentUser = firebase.auth().currentUser;
10867      if (!currentUser.emailVerified) {
10868        this.setState({ emailVerificationBusy: false, emailVerificationMessage: silent ?this.state.emailVerificationMessage : 'Todavía no figura confirmado. Abrí el enlace del correo y volvé a intentar.' });
10869        return;
10870      }
10871      await currentUser.getIdToken(true).catch(() => null);
10872      this.setState({ emailVerificationMessage: 'Correo verificado. Ingresando…' });
10873      await this.handleAuthenticatedUser(currentUser, { skipReload: true });
10874    } catch (error) {
10875      this.setState({ emailVerificationBusy: false, emailVerificationMessage: error?.message || 'No pudimos comprobar el correo. Probá nuevamente.' });
10876    }
10877  }
10878  async resendEmailVerification() {
10879    if (this.state.emailVerificationBusy) return;
10880    this.setState({ emailVerificationBusy: true, emailVerificationMessage: '' });
10881    try {
10882      const firebase = await this.ensureFirebase();
10883      const user = firebase.auth().currentUser;
10884      if (!user) throw new Error('La sesión venció. Volvé a ingresar.');
10885      if (user.emailVerified) {
10886        this.setState({ emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '' });
10887        await this.handleAuthenticatedUser(firebase.auth().currentUser || user, { skipReload: true });
10888        return;
10889      }
10890      await this.sendRegistrationVerification(user);
10891      this.setState({ emailVerificationBusy: false, emailVerificationMessage: 'Correo aceptado para entrega. Revisá Recibidos y también Spam o Correo no deseado.' });
10892    } catch (error) {
10893      this.setState({ emailVerificationBusy: false, emailVerificationMessage: this.authError(error) });
10894    }
10895  }
10896  async logoutFromEmailVerification() {
10897    if (this.state.emailVerificationBusy) return;
10898    this.clearWelcomeAfterSignIn();
10899    this.clearCachedApiUser();
10900    try { sessionStorage.removeItem('rosgan:lastRoute'); } catch {}
10901    await this.signOutSession();
10902    this.setState({ user: null, route: 'login', authReady: true, authMode: 'signin', emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', phoneVerificationRequired: false });
10903  }
10904  requestWelcomeAfterSignIn() {
10905    this._welcomeStartedAt = Date.now();
10906    try {
10907      sessionStorage.setItem('rosgan:welcomeAfterSignIn', '1');
10908      sessionStorage.setItem('rosgan:welcomeStartedAt', String(this._welcomeStartedAt));
10909    } catch {}
10910    this.setState({ authWelcomeRequested: true });
10911  }
10912  consumeWelcomeAfterSignIn() {
10913    let requested = !!this.state.authWelcomeRequested;
10914    try {
10915      requested = requested || sessionStorage.getItem('rosgan:welcomeAfterSignIn') === '1';
10916      const storedStart = Number(sessionStorage.getItem('rosgan:welcomeStartedAt'));
10917      if (!this._welcomeStartedAt && Number.isFinite(storedStart) && storedStart > 0) this._welcomeStartedAt = storedStart;
10918      sessionStorage.removeItem('rosgan:welcomeAfterSignIn');
10919      sessionStorage.removeItem('rosgan:welcomeStartedAt');
10920    } catch {}
10921    return requested;
10922  }
10923  clearWelcomeAfterSignIn() {
10924    this._welcomeStartedAt = null;
10925    try {
10926      sessionStorage.removeItem('rosgan:welcomeAfterSignIn');
10927      sessionStorage.removeItem('rosgan:welcomeStartedAt');
10928    } catch {}
10929    if (this.state.authWelcomeRequested) this.setState({ authWelcomeRequested: false });
10930  }
10931  hasPendingWelcome() {
10932    if (this.state.authWelcomeRequested) return true;
10933    try { return sessionStorage.getItem('rosgan:welcomeAfterSignIn') === '1'; } catch { return false; }
10934  }
10935  readCachedApiUser(user) {
10936    if (!user?.uid) return null;
10937    try {
10938      const cached = JSON.parse(sessionStorage.getItem('rosgan:authenticatedApiUser') || 'null');
10939      if (!cached || cached.uid !== user.uid || Number(cached.expiresAt || 0) <= Date.now()) return null;
10940      const apiUser = this.normalizeApiSessionUser(cached.apiUser);
10941      return apiUser && this.apiRoleIds(apiUser).length ?apiUser : null;
10942    } catch { return null; }
10943  }
10944  cacheApiUser(user, apiUser) {
10945    if (!user?.uid || !apiUser || !this.apiRoleIds(apiUser).length) return;
10946    try {
10947      sessionStorage.setItem('rosgan:authenticatedApiUser', JSON.stringify({
10948        uid: user.uid,
10949        expiresAt: Date.now() + 6 * 60 * 60 * 1000,
10950        apiUser,
10951      }));
10952    } catch {}
10953  }
10954  clearSessionIdentity() {
10955    this._sessionEpoch = (this._sessionEpoch || 0) + 1;
10956    this._pathRequest = (this._pathRequest || 0) + 1;
10957    this.clearCachedApiUser();
10958    this._userConsignatariaPromise = null;
10959    this._userConsignatariaUid = null;
10960    this.setState({ user: null, apiUser: null, apiSessionError: '', userConsignataria: null,
10961      userConsignatarias: [], myLots: null, myBids: null, profileFetchInfo: {},
10962      profileRosganLots: [], profilePhotoFailed: false, profilePhotoViewerOpen: false,
10963      notificationsItems: null, notificationsOpen: false });
10964  }
10965  async signOutSession() {
10966    this._signingOut = true;
10967    this.clearSessionIdentity();
10968    try { await (await this.ensureFirebase()).auth().signOut(); }
10969    catch { this.setState({ authMessage: 'No pudimos cerrar la sesión. Volvé a intentar salir.' }); }
10970    finally { this._signingOut = false; }
10971  }
10972  clearCachedApiUser() {
10973    try { sessionStorage.removeItem('rosgan:authenticatedApiUser'); } catch {}
10974  }
10975  readCollectionCache(key) {
10976    if (!key) return null;
10977    try {
10978      const cached = JSON.parse(sessionStorage.getItem(key) || 'null');
10979      if (!cached || Number(cached.expiresAt || 0) <= Date.now()) return null;
10980      return cached.value ?? null;
10981    } catch { return null; }
10982  }
10983  writeCollectionCache(key, value, ttlMs = 30 * 60 * 1000) {
10984    if (!key) return;
10985    try {
10986      const serialized = JSON.stringify({ expiresAt: Date.now() + ttlMs, value }, (_key, item) => {
10987        if (typeof item === 'string' && item.startsWith('data:') && item.length > 10000) return '';
10988        return item;
10989      });
10990      sessionStorage.setItem(key, serialized);
10991    } catch {}
10992  }
10993  runViewTransition(update) {
10994    const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
10995    if (reduceMotion) return Promise.resolve().then(update);
10996    if (!document.startViewTransition) {
10997      return new Promise((resolve, reject) => {
10998        this.setState({ authTransitioning: true }, () => {
10999          setTimeout(() => {
11000            Promise.resolve().then(update).then((result) => {
11001              setTimeout(() => this.setState({ authTransitioning: false }, () => resolve(result)), 340);
11002            }).catch((error) => {
11003              this.setState({ authTransitioning: false }, () => reject(error));
11004            });
11005          }, 220);
11006        });
11007      });
11008    }
11009    let transition;
11010    try {
11011      transition = document.startViewTransition(() => Promise.resolve().then(update));
11012    } catch {
11013      return Promise.resolve().then(update);
11014    }
11015    transition.finished.catch(() => {});
11016    return transition.updateCallbackDone;
11017  }
11018  transitionState(nextState) {
11019    return this.runViewTransition(() => new Promise((resolve) => this.setState(nextState, resolve)));
11020  }
11021  async finishWelcome(targetRoute = 'home', initialPath = '') {
11022    this._welcomeTimer = null;
11023    if (this._welcomeBindTimer) clearTimeout(this._welcomeBindTimer);
11024    this._welcomeBindTimer = null;
11025    this._welcomeStartedAt = null;
11026    if (this._welcomeLottiePlayer && this._welcomeLottieComplete) {
11027      this._welcomeLottiePlayer.removeEventListener('complete', this._welcomeLottieComplete);
11028    }
11029    if (this._welcomeDotLottie && this._welcomeLottieComplete) {
11030      this._welcomeDotLottie.removeEventListener('complete', this._welcomeLottieComplete);
11031    }
11032    this._welcomeLottiePlayer = null;
11033    this._welcomeDotLottie = null;
11034    this._welcomeLottieComplete = null;
11035    this._welcomeAnimationComplete = false;
11036    this._welcomeFinishWhenReady = null;
11037    if (this.state.route !== 'welcome') return;
11038    const roslotLotTarget = this.canAccessRoslot() ?this.roslotLotPathTarget(initialPath) : null;
11039    const deepAppPath = roslotLotTarget || this.rematePathTarget(initialPath) || this.cabanaPathTarget(initialPath);
11040    if (deepAppPath) {
11041      const initialUrl = initialPath + (this._initialSearch || '');
11042      await this.runViewTransition(() => new Promise((resolve) => {
11043        try { window.history.replaceState({}, '', initialUrl); } catch {}
11044        this.setState({ routeLoading: true }, () => {
11045          this.navigateFromPath(initialPath);
11046          requestAnimationFrame(resolve);
11047        });
11048      }));
11049      return;
11050    }
11051    const nextRoute = targetRoute || 'home';
11052    await this.runViewTransition(() => new Promise((resolve) => {
11053      this.syncAppLocation(nextRoute, { replace: true });
11054      this.go(nextRoute, { syncPath: false });
11055      requestAnimationFrame(resolve);
11056    }));
11057  }
11058  async activateAuthenticatedUser(user, bootstrapApiUser = null) {
11059    const sessionEpoch = this._sessionEpoch || 0;
11060    const enteringFromLogin = this.state.authReady && this.state.route === 'login';
11061    const welcomeRequested = this.consumeWelcomeAfterSignIn();
11062    const requestedRoute = this._initialPathRoute || '';
11063    const initialPath = this._initialPathname || window.location.pathname;
11064    const initialUrl = initialPath + (this._initialSearch || '');
11065    const preserveReadyView = !!bootstrapApiUser && this.state.authReady && !this.state.routeLoading && this.state.route !== 'login' && !welcomeRequested;
11066    const authenticatedState = {
11067      user,
11068      publicAdvanceMode: this.state.publicAdvanceMode === 'cabanas' ? '' : this.state.publicAdvanceMode,
11069      authReady: preserveReadyView,
11070      authBusy: false,
11071      authMessage: '',
11072      emailVerificationRequired: false,
11073      emailVerificationBusy: false,
11074      emailVerificationMessage: '',
11075      phoneVerificationRequired: false,
11076      phoneVerificationBusy: false,
11077      phoneVerificationMessage: '',
11078      phoneVerificationNumber: user.phoneNumber || this.state.phoneVerificationNumber,
11079    };
11080    // authVerificationAccess already synchronized the user and returned the API
11081    // session. Avoid repeating both remote calls after every Google sign-in.
11082    const minimumConnectingTime = welcomeRequested
11083      ? new Promise((resolve) => setTimeout(resolve, 900))
11084      : Promise.resolve();
11085    if (enteringFromLogin) await this.transitionState(authenticatedState);
11086    else await new Promise((resolve) => this.setState(authenticatedState, resolve));
11087
11088    const validationPromise = bootstrapApiUser
11089      ? Promise.resolve([null, bootstrapApiUser])
11090      : Promise.all([
11091          this.syncAuthUser(user),
11092          this.fetchApiSession(),
11093        ]);
11094    const [, initialApiUser] = await validationPromise;
11095    if (sessionEpoch !== (this._sessionEpoch || 0) || this.state.user?.uid !== user.uid) return;
11096    await minimumConnectingTime;
11097    if (sessionEpoch !== (this._sessionEpoch || 0) || this.state.user?.uid !== user.uid) return;
11098    const apiUser = initialApiUser || bootstrapApiUser || await this.fetchApiSession();
11099    if (sessionEpoch !== (this._sessionEpoch || 0) || this.state.user?.uid !== user.uid) return;
11100    if (apiUser) {
11101      this.cacheApiUser(user, apiUser);
11102      if (apiUser !== this.state.apiUser) this.setState({ apiUser });
11103    }
11104    const catalogReturn = this.cabanaPathTarget(initialPath);
11105    const returningToCatalog = ['cabanaDetail', 'cabanaLotDetail'].includes(requestedRoute) && ['detail', 'lot'].includes(catalogReturn?.action) && !user.isAnonymous;
11106    const canAccess = this.canAccessApp(apiUser) || returningToCatalog;
11107    const requestedRouteAllowed = !requestedRoute || this.canAccessRoute(requestedRoute, apiUser);
11108    const targetRoute = canAccess ?(requestedRouteAllowed ?(requestedRoute || 'home') : 'home') : 'home';
11109    const showWelcome = canAccess && welcomeRequested;
11110    const route = showWelcome ?'welcome' : targetRoute;
11111    const roslotDeepPath = this.roslotLotPathTarget(initialPath);
11112    const remateDeepPath = this.rematePathTarget(initialPath);
11113    const cabanaDeepPath = this.cabanaPathTarget(initialPath);
11114    const deepRoute = roslotDeepPath ?'pool' : (remateDeepPath ?'remates' : (cabanaDeepPath ?'cabanas' : ''));
11115    const deepAppPath = canAccess && this.canAccessRoute(deepRoute, apiUser) && (roslotDeepPath || remateDeepPath || cabanaDeepPath);
11116    try {
11117      const welcomePath = requestedRoute ?(deepAppPath ?initialUrl : this.appPathForRoute(targetRoute)) : '/';
11118      const nextUrl = canAccess ?(showWelcome ?welcomePath : (deepAppPath ?initialUrl : this.appPathForRoute(targetRoute))) : '/';
11119      if (!preserveReadyView) window.history.replaceState({}, '', nextUrl);
11120    } catch {}
11121    const readyState = {
11122      route: preserveReadyView ? this.state.route : route,
11123      routeLoading: preserveReadyView ? !!this.state.routeLoading : !showWelcome && !!deepAppPath,
11124      authReady: true,
11125      authWelcomeRequested: false,
11126      mailingOpen: false,
11127      adminUsersOpen: false,
11128    };
11129    if (route === 'welcome' || preserveReadyView) await new Promise((resolve) => this.setState(readyState, resolve));
11130    else await this.transitionState(readyState);
11131// Resolve deep links as soon as authentication is ready. Profile metrics and
11132    // secondary catalogs can keep loading in parallel without leaving the route blank.
11133    if (!showWelcome && deepAppPath && !preserveReadyView) this.navigateFromPath(initialPath);
11134    if (showWelcome) {
11135      requestAnimationFrame(() => {
11136        const player = document.querySelector('.app-welcome-lottie');
11137        let completed = false;
11138        let bindAttempts = 0;
11139        const continueToApp = () => {
11140          if (completed || this.state.route !== 'welcome') return;
11141          completed = true;
11142          if (this._welcomeTimer) clearTimeout(this._welcomeTimer);
11143          if (this._welcomeBindTimer) clearTimeout(this._welcomeBindTimer);
11144          this.finishWelcome(targetRoute, initialPath);
11145        };
11146        const bindDotLottie = () => {
11147          const dotLottie = player?.dotLottie || null;
11148          if (!dotLottie && bindAttempts < 20 && this.state.route === 'welcome') {
11149            bindAttempts += 1;
11150            this._welcomeBindTimer = setTimeout(bindDotLottie, 50);
11151            return;
11152          }
11153          this._welcomeDotLottie = dotLottie;
11154          dotLottie?.addEventListener('complete', continueToApp);
11155          const lottieAlreadyFinished = !!dotLottie
11156            && dotLottie.isLoaded
11157            && !dotLottie.isPlaying
11158            && dotLottie.totalFrames > 0
11159            && dotLottie.currentFrame >= dotLottie.totalFrames - 2;
11160          if (lottieAlreadyFinished) continueToApp();
11161        };
11162        this._welcomeLottiePlayer = player;
11163        this._welcomeLottieComplete = continueToApp;
11164        player?.addEventListener('complete', continueToApp, { once: true });
11165        bindDotLottie();
11166        this._welcomeTimer = setTimeout(continueToApp, 2850);
11167      });
11168    }
11169    if (targetRoute === 'cabanas' && !this.state.cabanasCollectionReady && !this.state.cabanasLoading) {
11170      await this.fetchCabanas(true);
11171    }
11172    if (targetRoute === 'rosganLotLoad') {
11173      this.loadRosganLotCatalogs();
11174    }
11175    this.loadCatalog();
11176    this.fetchData();
11177    this.fetchLots();
11178    this.initializeNotifications();
11179    this.fetchNotifications(false);
11180    this.fetchWhatsAppNotificationPreference(true);
11181    if (targetRoute !== 'cabanas' && !this.state.cabanas && !this.state.cabanasLoading) {
11182      const warmCabanaCards = () => this.fetchCabanas();
11183      if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(warmCabanaCards, { timeout: 1200 });
11184      else setTimeout(warmCabanaCards, 200);
11185    }
11186    if (targetRoute !== 'home') await this.refreshProfileData();
11187    if (!showWelcome && this.state.route === 'profile') {
11188      this.animateDash();
11189    }
11190  }
11191  async fetchLaunchInterest() {
11192    if (!this.state.user || this.state.launchInterestLoading) return;
11193    this.setState({ launchInterestLoading: true, launchInterestMessage: '' });
11194    try {
11195      const data = await this.apiFetch('/api/launch-interests?mine=1');
11196      this.setState({ launchInterest: data?.interest || data?.data?.interest || null, launchInterestLoading: false });
11197    } catch (error) {
11198      this.setState({ launchInterestLoading: false, launchInterestMessage: error?.message || 'No pudimos consultar tu aviso.' });
11199    }
11200  }
11201  async toggleLaunchInterest() {
11202    if (!this.state.user || this.state.launchInterestSaving) return;
11203    const subscribed = this.state.launchInterest?.status === 'subscribed';
11204    this.setState({ launchInterestSaving: true, launchInterestMessage: '' });
11205    try {
11206      const data = await this.apiFetch('/api/launch-interests', subscribed
11207        ? { method: 'DELETE' }
11208        : { method: 'POST', body: JSON.stringify({ consent: true, emailOptIn: true, whatsappOptIn: true }) });
11209      const interest = data?.interest || data?.data?.interest || { status: subscribed ?'unsubscribed' : 'subscribed' };
11210      this.setState({
11211        launchInterest: interest,
11212        launchInterestSaving: false,
11213        launchInterestMessage: subscribed
11214          ? 'Listo. Dejamos de enviarte este aviso.'
11215          : '¡Listo! Te avisaremos cuando Rosgan esté disponible.',
11216      });
11217    } catch (error) {
11218      this.setState({ launchInterestSaving: false, launchInterestMessage: error?.message || 'No pudimos guardar tu elección.' });
11219    }
11220  }
11221  async fetchLaunchInterestsAdmin() {
11222    if (!this.canManageUsers() || this.state.launchInterestsAdminLoading) return;
11223    this.setState({ launchInterestsAdminLoading: true, launchInterestsAdminError: '' });
11224    try {
11225      const data = await this.apiFetch('/api/launch-interests');
11226      const interests = Array.isArray(data?.interests) ?data.interests : (Array.isArray(data?.data?.interests) ?data.data.interests : []);
11227      this.setState({
11228        launchInterestsAdmin: interests,
11229        launchInterestsAdminTotal: Number(data?.total ?? data?.data?.total) || interests.length,
11230        launchInterestsAdminLoading: false,
11231      });
11232    } catch (error) {
11233      this.setState({ launchInterestsAdminLoading: false, launchInterestsAdminError: error?.message || 'No pudimos cargar los interesados.' });
11234    }
11235  }
11236  exportLaunchInterests() {
11237    const interests = Array.isArray(this.state.launchInterestsAdmin) ?this.state.launchInterestsAdmin : [];
11238    if (!interests.length) return;
11239    const cell = (value) => '"' + String(value ?? '').replace(/"/g, '""') + '"';
11240    const rows = [['Nombre', 'Email', 'Teléfono', 'Email', 'WhatsApp', 'Fecha de alta']]
11241      .concat(interests.map((item) => [
11242        item.displayName || '', item.email || '', item.phoneNumber || '', item.emailOptIn ?'Sí' : 'No',
11243        item.whatsappOptIn ?'Sí' : 'No', item.updatedAt || item.createdAt || '',
11244      ]))
11245      .map((row) => row.map(cell).join(','));
11246    this.downloadCabanaBlob(new Blob(['\ufeff', rows.join('\r\n')], { type: 'text/csv;charset=utf-8' }), 'interesados-lanzamiento-rosgan.csv');
11247  }
11248  async authVerificationAccess(user) {
11249    if (!user) return { bypass: false, apiUser: null };
11250    const cachedApiUser = this.readCachedApiUser(user);
11251    let tokenClaims = null;
11252    try {
11253      const [token, tokenResult] = await Promise.all([
11254        user.getIdToken(),
11255        user.getIdTokenResult?.().catch(() => null),
11256      ]);
11257      tokenClaims = tokenResult?.claims || null;
11258      const response = await fetch('/api/auth/sync-user', {
11259        method: 'POST',
11260        headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
11261        body: JSON.stringify({
11262          displayName: user.displayName || '',
11263          email: user.email || '',
11264          phoneNumber: user.phoneNumber || '',
11265          phoneVerified: Boolean(user.phoneNumber),
11266        }),
11267      });
11268      const data = await response.json().catch(() => null);
11269      if (!response.ok || !data) {
11270        const claimsRoles = this.userRoleIds(tokenClaims);
11271        const claimsUser = claimsRoles.length ?{
11272          uid: user.uid,
11273          email: user.email || '',
11274          roles: claimsRoles,
11275          roleId: claimsRoles,
11276          role: ['sysadmin', 'admin', 'dev', 'seller', 'buyer'].find((role) => claimsRoles.includes(role)) || 'user',
11277        } : null;
11278        const fallback = cachedApiUser || claimsUser;
11279        return { bypass: this.userRoleIds(fallback).includes('dev'), apiUser: fallback };
11280      }
11281      const session = data.session || data.data?.session || data.data || data;
11282      const external = this.normalizeApiSessionUser(session.apiSession || data.apiSession || session);
11283      const stored = session.user && typeof session.user === 'object' ?session.user : null;
11284      const roles = [...new Set([
11285        ...this.userRoleIds(data),
11286        ...this.userRoleIds(session),
11287        ...this.userRoleIds(stored),
11288        ...this.userRoleIds(external),
11289        ...this.userRoleIds(tokenClaims),
11290      ])];
11291      const primaryRole = ['sysadmin', 'admin', 'seller', 'cabanero', 'buyer', 'dev'].find((role) => roles.includes(role))
11292        || external?.role || session.role || stored?.role || 'user';
11293      const apiUser = {
11294        ...(stored || {}),
11295        ...(external || {}),
11296        phoneNumber: session.phoneNumber || stored?.phoneNumber || user.phoneNumber || '',
11297        uid: external?.uid || stored?.uid || session.uid || user.uid,
11298        email: external?.email || stored?.email || session.email || user.email || '',
11299        roles,
11300        roleId: roles,
11301        role: primaryRole,
11302      };
11303      this.cacheApiUser(user, apiUser);
11304      return { bypass: session.bypassVerification === true || roles.includes('dev'), apiUser };
11305    } catch (error) {
11306      console.warn('auth verification access', error);
11307      const claimsRoles = this.userRoleIds(tokenClaims);
11308      const claimsUser = claimsRoles.length ?{
11309        uid: user.uid,
11310        email: user.email || '',
11311        roles: claimsRoles,
11312        roleId: claimsRoles,
11313        role: ['sysadmin', 'admin', 'dev', 'seller', 'buyer'].find((role) => claimsRoles.includes(role)) || 'user',
11314      } : null;
11315      const fallback = cachedApiUser || claimsUser;
11316      return { bypass: this.userRoleIds(fallback).includes('dev'), apiUser: fallback };
11317    }
11318  }
11319  async handleAuthenticatedUser(user, options = {}) {
11320    if (this._signingOut || !user?.uid) return;
11321    const epoch = this._sessionEpoch || 0;
11322    const key = user.uid + ':' + epoch;
11323    if (this._authHandling?.key === key) return this._authHandling.promise;
11324    if (this.state.user?.uid === user.uid && this.state.authReady && this.state.apiUser && !this.state.phoneVerificationRequired && !options.skipReload) {
11325      this.setState({ user });
11326      return;
11327    }
11328    if (this.state.user && this.state.user.uid !== user.uid) this.clearSessionIdentity();
11329    const promise = this.restoreAuthenticatedUser(user, options);
11330    this._authHandling = { key, promise };
11331    try { return await promise; }
11332    finally { if (this._authHandling?.promise === promise) this._authHandling = null; }
11333  }
11334  async restoreAuthenticatedUser(user, options = {}) {
11335    const sessionEpoch = this._sessionEpoch || 0;
11336    // Una recarga no debe bloquear la interfaz mientras se repiten las mismas
11337    // consultas remotas. Restauramos el rol ya validado para este UID y luego
11338    // refrescamos todos los datos con los endpoints autenticados habituales.
11339    const cachedApiUser = this.readCachedApiUser(user);
11340    if (cachedApiUser && (this.userRoleIds(cachedApiUser).includes('dev') || this.phoneNumberE164(user.phoneNumber || cachedApiUser.phoneNumber))) {
11341      const canAccess = this.canAccessApp(cachedApiUser);
11342      const targetRoute = canAccess ?(this._initialPathRoute || this._initialRoute || 'home') : 'home';
11343      await new Promise((resolve) => this.setState({
11344        user,
11345        apiUser: cachedApiUser,
11346        route: targetRoute,
11347        routeLoading: canAccess && !!(this.rematePathTarget(this._initialPathname) || this.cabanaPathTarget(this._initialPathname) || this.roslotLotPathTarget(this._initialPathname)),
11348        authReady: true,
11349        authBusy: false,
11350        authMessage: '',
11351        emailVerificationRequired: false,
11352        phoneVerificationRequired: false,
11353      }, resolve));
11354      if (this._signingOut || sessionEpoch !== (this._sessionEpoch || 0)) return;
11355      await this.activateAuthenticatedUser(user, cachedApiUser);
11356      return;
11357    }
11358    // Once Firebase hands back the Google session, move off the login form
11359    // immediately while role and verification checks continue in parallel.
11360    if (this.state.authWelcomeRequested && this.state.authReady) {
11361      await new Promise((resolve) => this.setState({
11362        user,
11363        authReady: false,
11364        authBusy: true,
11365        authMessage: '',
11366      }, resolve));
11367    }
11368    if (!options?.skipReload) {
11369      try { await this.authOperationWithTimeout(user.reload(), 8000, ''); } catch {}
11370    }
11371    const firebase = await this.ensureFirebase();
11372    const currentUser = firebase.auth().currentUser;
11373    if (this._signingOut || sessionEpoch !== (this._sessionEpoch || 0) || currentUser?.uid !== user.uid) return;
11374    const verificationAccess = await this.authVerificationAccess(currentUser);
11375    if (sessionEpoch !== (this._sessionEpoch || 0) || firebase.auth().currentUser?.uid !== currentUser.uid) return;
11376    if (!verificationAccess.bypass && !this.phoneNumberE164(currentUser.phoneNumber || verificationAccess.apiUser?.phoneNumber)) {
11377      const prefilled = this.phoneNumberE164(this.state.formPhone) || this.phoneNumberE164(this.state.phoneVerificationNumber) || '+54';
11378      this.setState({
11379        user: currentUser,
11380        route: this.state.route === 'login' ?(this._initialRoute || 'profile') : this.state.route,
11381        authReady: true,
11382        authBusy: false,
11383        authMessage: '',
11384        emailVerificationRequired: false,
11385        emailVerificationBusy: false,
11386        emailVerificationMessage: '',
11387        phoneVerificationRequired: true,
11388        phoneVerificationStep: 'phone',
11389        phoneVerificationNumber: prefilled,
11390        phoneVerificationCode: '',
11391        phoneVerificationBusy: false,
11392        phoneVerificationMessage: '',
11393      });
11394      return;
11395    }
11396    await this.activateAuthenticatedUser(currentUser, verificationAccess.apiUser);
11397  }
11398  async componentDidMount() {
11399    window.rosganAudienceApp = this;
11400    const configureFieldMode = () => { const uploadBridge = {
11401      user: () => this.state.user && this.state.apiUser ? { uid: this.state.user.uid } : null,
11402      request: async (uid, path, options = {}) => {
11403        const token = await this.authToken();
11404        if (!token || this.state.user?.uid !== uid || !this.state.apiUser) throw Object.assign(new Error('Iniciá sesión con la cuenta que filmó.'), { status: 401 });
11405        const response = await fetch(this.API + path, { ...options, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token } });
11406        if (!response.ok) {
11407          const data = await response.json().catch(() => null);
11408          const detail = data?.message || data?.detail || (typeof data?.error === 'string' ? data.error : '');
11409          throw Object.assign(new Error('No se pudo confirmar la operación.'), { status: response.status, detail: typeof detail === 'string' ? detail : '' });
11410        }
11411        const text = await response.text();
11412        return text.trim() ? JSON.parse(text) : null;
11413      },
11414    }; window.RosganField.configure(uploadBridge); window.RosganVideoQueue?.configure(uploadBridge); };
11415    if (window.RosganField) configureFieldMode();
11416    else window.addEventListener('rosgan-field-ready', configureFieldMode, { once: true });
11417    this._onTextInputCapture = (event) => {
11418      if (!this.isTextEntryControl(event.target)) return;
11419      this._handlingTextInput = true;
11420      queueMicrotask(() => { this._handlingTextInput = false; });
11421    };
11422    this._onFormCompositionStart = (event) => { if (this.isTextEntryControl(event.target)) this._formCompositionActive = true; };
11423    this._onFormCompositionEnd = (event) => {
11424      if (!this.isTextEntryControl(event.target)) return;
11425      this._formCompositionActive = false;
11426      this.scheduleTextEntryRender(40);
11427    };
11428    document.addEventListener('input', this._onTextInputCapture, true);
11429    document.addEventListener('compositionstart', this._onFormCompositionStart, true);
11430    document.addEventListener('compositionend', this._onFormCompositionEnd, true);
11431    const publicMode = this.state.publicAdvanceMode;
11432    const legacyAppPath = this.legacyRoslotHashPath(window.location.hash) || this.legacyCabanaHashPath(window.location.hash);
11433    this._initialPathname = legacyAppPath || window.location.pathname;
11434    this._initialSearch = legacyAppPath ?'' : window.location.search;
11435    this._initialPathRoute = publicMode ?'' : this.routeFromPath(this._initialPathname);
11436    this._initialRoute = window.__ROSGAN_LOCAL_MAILING_ENTRY__ ?'mailingAdmin' : ((this.props.iniciarEnHome || false) ?'home' : this.restoreRoute());
11437    if (publicMode === 'lacassina') {
11438      const wantsStats = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-stats' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/estadisticas';
11439      const wantsCommercial = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-commercial' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/comercial';
11440      const wantsPrices = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-prices' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/precios';
11441      const wantsTv = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-tv' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv';
11442      const wantsTvConfig = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-tv-config' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv/config';
11443      const params = new URLSearchParams(window.location.search || '');
11444      const focusRp = window.__ROSGAN_PUBLIC_RP__ || params.get('rp') || '';
11445      const focusLotNumber = window.__ROSGAN_PUBLIC_LOT_NUMBER__ || params.get('lote') || '';
11446      this.releaseProfileCrop();
11447      this.setState({
11448        route: wantsStats ? 'cabanaStats' : (wantsCommercial ? 'cabanaCommercial' : (wantsPrices ? 'cabanaPriceBoard' : (wantsTvConfig ? 'cabanaTvConfig' : (wantsTv ? 'cabanaTvLower' : (focusRp ? 'cabanaDetail' : 'cabanas'))))),
11449        authReady: true,
11450        user: null,
11451        profileEditorOpen: false,
11452        profilePhotoViewerOpen: false,
11453        profileCropOpen: false,
11454        profileCropSource: '',
11455        adminUsersOpen: false,
11456        notificationsOpen: false,
11457        cabanasLoading: true,
11458        cabanaLotsLoading: wantsStats || wantsCommercial || wantsPrices || wantsTv || wantsTvConfig,
11459        cabanaPublicDeepLoading: !!focusRp
11460      }, async () => {
11461        try {
11462          const remate = await this.fetchCabanaById('2', { force: true });
11463          if (!remate) throw new Error('No se encontró el remate de La Cassina.');
11464          if (wantsStats) this.openCabanaStats(remate);
11465          else if (wantsCommercial) this.openCabanaCommercial(remate);
11466          else if (wantsPrices) this.openCabanaPriceBoard(remate);
11467          else if (wantsTvConfig) this.openCabanaTvConfig(remate);
11468          else if (wantsTv) this.openCabanaTvLower(remate);
11469          else this.openCabana(remate, { focusRp, focusLotNumber });
11470        } catch (error) {
11471          this.setState({ cabanasError: error?.message || 'No se pudo cargar el adelanto de La Cassina.', cabanasLoading: false, cabanaLotsLoading: false });
11472        }
11473      });
11474    } else {
11475      if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer);
11476      this._authBootstrapTimer = setTimeout(() => {
11477        this._authBootstrapTimer = null;
11478        if (this.state.authReady) return;
11479        if (this.state.user || window.firebase?.auth?.().currentUser) return;
11480        this.setState({
11481          route: 'login',
11482          authReady: true,
11483          authBusy: false,
11484          authMessage: 'La conexión demoró más de lo esperado. Ya podés intentar ingresar.',
11485          emailVerificationRequired: false,
11486          phoneVerificationRequired: false,
11487        });
11488      }, 12000);
11489      try {
11490        const firebase = await this.ensureFirebase();
11491        const auth = firebase.auth();
11492        this._authUnsubscribe = auth.onAuthStateChanged(async (u) => {
11493          if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer);
11494          this._authBootstrapTimer = null;
11495          if (u) {
11496            if (this._signingOut || auth.currentUser?.uid !== u.uid) return;
11497            try { await this.handleAuthenticatedUser(u); }
11498            catch { this.setState({ authBusy: false, authReady: true, authMessage: 'No pudimos cargar tu sesión. Revisá la conexión y recargá la página.' }); }
11499          } else {
11500            // Ignore a stale empty callback if Firebase already recovered the
11501            // durable user while this callback was queued during page startup.
11502            if (auth.currentUser) {
11503              await this.handleAuthenticatedUser(auth.currentUser);
11504              return;
11505            }
11506            this.clearSessionIdentity();
11507            this.setState({ routeLoading: false });
11508            this.clearCachedApiUser();
11509            this._phoneConfirmationResult = null;
11510            this._phoneVerificationId = null;
11511            this.clearPhoneRecaptcha();
11512            if (this.activatePublicCatalogGuest(window.location.pathname)) return;
11513            this.setState({ user: null, route: 'login', authReady: true, authBusy: false, emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', phoneVerificationRequired: false, phoneVerificationBusy: false, phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationMessage: '', notificationsItems: null, notificationsOpen: false });
11514          }
11515        });
11516        auth.getRedirectResult().then(async (r) => {
11517          if (r && r.user) {
11518            await this.persistFirebaseUser(firebase, r.user);
11519            this.setState({ authBusy: false, authMessage: '' });
11520          }
11521        }).catch((e) => { const m = this.googleErr(e); if (e?.code) this.setState({ authBusy: false, authMessage: m }); });
11522      } catch (e) {
11523        if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer);
11524        this._authBootstrapTimer = null;
11525        console.warn('firebase init', e);
11526        if (this.state.user || window.firebase?.auth?.().currentUser) return;
11527        this.setState({ route: 'login', authReady: true, authBusy: false, authMessage: 'No pudimos restaurar tu sesión. Probá nuevamente.' });
11528      }
11529    }
11530    this._onResize = () => {
11531      const isMobile = window.innerWidth < 760;
11532      const compactNavigation = window.innerWidth <= 1024;
11533      if (isMobile !== this.state.isMobile || compactNavigation !== this.state.compactNavigation) this.setState({ isMobile, compactNavigation }, () => this.syncCatalogFloatingContact());
11534      else this.syncCatalogFloatingContact();
11535    };
11536    window.addEventListener('resize', this._onResize);
11537    this._onRoslotTouchStart = (event) => {
11538      if (!this.state.isMobile || this.state.route !== 'pool' || event.touches?.length !== 1) return;
11539      const target = event.target;
11540      if (target?.closest?.('input, textarea, select, [data-drag-scroll]')) return;
11541      const touch = event.touches[0];
11542      this._roslotSwipeStart = { x: touch.clientX, y: touch.clientY, at: Date.now() };
11543    };
11544    this._onRoslotTouchEnd = (event) => {
11545      const start = this._roslotSwipeStart;
11546      this._roslotSwipeStart = null;
11547      if (!start || !this.state.isMobile || this.state.route !== 'pool' || event.changedTouches?.length !== 1) return;
11548      const touch = event.changedTouches[0];
11549      const dx = touch.clientX - start.x;
11550      const dy = touch.clientY - start.y;
11551      const horizontal = Math.abs(dx) >= 58 && Math.abs(dx) > Math.abs(dy) * 1.25;
11552      const quickEnough = Date.now() - start.at < 1400;
11553      if (!horizontal || !quickEnough) return;
11554      if (!this.state.roslotMobilePanelOpen && dx < 0) this.openRoslotMobilePanel();
11555      else if (this.state.roslotMobilePanelOpen && dx > 0) this.closeRoslotMobilePanel();
11556    };
11557    document.addEventListener('touchstart', this._onRoslotTouchStart, { passive: true });
11558    document.addEventListener('touchend', this._onRoslotTouchEnd, { passive: true });
11559    this._onHashChange = () => this.navigateFromHash();
11560    window.addEventListener('hashchange', this._onHashChange);
11561    this._onPopState = () => {
11562      if (this.state.publicAdvanceMode === 'lacassina') this.handleLacassinaPopState();
11563      else this.navigateFromPath();
11564    };
11565    window.addEventListener('popstate', this._onPopState);
11566    this._onEmailVerificationFocus = () => {
11567      if (this.state.emailVerificationRequired && !this.state.emailVerificationBusy) this.checkEmailVerification({ silent: true });
11568    };
11569    window.addEventListener('focus', this._onEmailVerificationFocus);
11570    this._onCabanaCommercialStorage = (event) => {
11571      if (this.state.route !== 'cabanaPriceBoard') return;
11572      if (event?.key && event.key !== this.cabanaCommercialStorageKey(this.state.currentCabana)) return;
11573      this.refreshCabanaPriceBoardDrafts();
11574    };
11575    window.addEventListener('storage', this._onCabanaCommercialStorage);
11576    this._onCabanaTvStorage = () => this.syncCabanaTvExternalState();
11577    window.addEventListener('storage', this._onCabanaTvStorage);
11578    window.addEventListener('rosgan:tv-config', this._onCabanaTvStorage);
11579    this._onRosganOpsStorage = (event) => {
11580      if (event?.key === this.rosganOpsStorageKey(this.state.currentRemate) + ':lot-preview') {
11581        this.receiveRosganTvPreview(event.newValue);
11582        return;
11583      }
11584      if (event?.key && event.key !== this.rosganOpsStorageKey(this.state.currentRemate)) return;
11585      this.syncRosganOpsExternalState();
11586    };
11587    window.addEventListener('storage', this._onRosganOpsStorage);
11588    window.addEventListener('rosgan:remate-operations', this._onRosganOpsStorage);
11589    this._rosganAirtimeTimer = setInterval(() => this.refreshRosganAirtime(), 1000);
11590    this._rosganOpsTimer = setInterval(() => {
11591      this.refreshRosganLotInterest();
11592      if (this.state.route !== 'rosganTvLower') { this.resetRosganTvDecks(); this.resetRosganTvMultiview(); }
11593      if (!['rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig'].includes(this.state.route)) return;
11594      this.syncRosganOpsExternalState();
11595      this.fetchRosganOpsRemoteState({ silent: true });
11596      if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo();
11597    }, 1200);
11598    this._rosganTvFieldsTimer = setInterval(() => {
11599      if (this.state.route === 'rosganTvLower' && this._rosganTvDecks?.onAirCert) {
11600        this.setState({ rosganTvFieldStep: (this.state.rosganTvFieldStep || 0) + 1 });
11601      }
11602    }, 6000);
11603    this._cabanaPriceBoardTimer = setInterval(() => {
11604      if (this.state.route === 'cabanaPriceBoard') this.refreshCabanaPriceBoardDrafts();
11605    }, 3000);
11606    this._cabanaStatsRefreshTimer = setInterval(() => {
11607      if (this.state.route === 'cabanaStats' && !this.state.cabanaStatsOverview && this.state.currentCabana && !this.state.cabanaAnalyticsLoading) {
11608        this.fetchCabanaAnalyticsSummary(this.state.currentCabana);
11609      }
11610    }, 10000);
11611    this._cabanaPrebidRealtimeTimer = setInterval(() => this.refreshCabanaPrebidRealtime(), 4000);
11612    this._prebidStartHandler = () => this.setState({ prebidBadgeTick: Date.now() });
11613    document.addEventListener('prebid-started', this._prebidStartHandler);
11614    this._prebidBadgeTimer = setInterval(() => {
11615      if (document.hidden || !['home', 'remates', 'cabanas', 'cabanaLotDetail'].includes(this.state.route)) return;
11616      const signature = [...(this.state.cabanas || []), this.state.currentCabana].map(item => window.CabanaPrebid.isOpen(item)).join(',');
11617      if (signature !== this._prebidBadgeSignature) { this._prebidBadgeSignature = signature; this.setState({ prebidBadgeTick: Date.now() }); }
11618    }, 15000);
11619    this._creditRealtimeTimer = setInterval(() => this.refreshCreditRealtime(), 4500);
11620    this._homeChannelFetchedAt = Date.now();
11621    this._homeChannelTimer = setInterval(() => this.refreshHomeChannel(), 30000);
11622    this._adminVersionTimer = setInterval(() => {
11623      if (!document.hidden && this.state.adminUsersOpen && !this.state.adminUserSaving) this.fetchAdminUsers(true);
11624    }, 60000);
11625    this._roslotPresenceTimer = setInterval(() => this.refreshRoslotPresence(), 60000);
11626    this._onScroll = () => {
11627      if (this.state.route === 'home') {
11628        const y = Math.min(window.scrollY, 600);
11629        const hero = document.querySelector('[data-hero-img]');
11630        if (hero) hero.style.transform = 'translateY(' + Math.round(y * 0.35) + 'px)';
11631      }
11632      this.syncCatalogFloatingContact();
11633    };
11634    window.addEventListener('scroll', this._onScroll, { passive: true });
11635    this._cabanaAnalyticsClickHandler = (event) => {
11636      const target = event.target?.closest?.('.dc-contact-whatsapp,.dc-contact-call');
11637      if (!target || this.state.route !== 'cabanaLotDetail') return;
11638      const whatsapp = target.classList.contains('dc-contact-whatsapp');
11639      const contact = this.cabanaContactData(this.state.currentCabana);
11640      this.trackCabanaAnalytics(whatsapp ?'contact_whatsapp_click' : 'contact_call_click', this.state.currentCabanaLot, {
11641        metadata: { action: whatsapp ?'contact_whatsapp' : 'contact_call', contactName: contact.name },
11642      });
11643    };
11644    document.addEventListener('click', this._cabanaAnalyticsClickHandler, true);
11645    this._onVisibilityChange = () => {
11646      if (document.hidden) this.flushCabanaLotViewTimer('tab_hidden');
11647      if (publicMode) return;
11648      if (!document.hidden) {
11649        if (this.state.emailVerificationRequired && !this.state.emailVerificationBusy) this.checkEmailVerification({ silent: true });
11650        if (this.state.user) this.refreshProfileData(true);
11651        this.syncAuctionStatuses();
11652        this.syncLiveBids();
11653        this.fetchNotifications(true);
11654        if (this.state.route === 'notifications') this.fetchNotificationHistory(true);
11655        this.refreshCabanaPrebidRealtime();
11656        this.refreshCreditRealtime({ refreshAuthorizations: true });
11657        this.refreshRoslotPresence();
11658      }
11659    };
11660    document.addEventListener('visibilitychange', this._onVisibilityChange);
11661    this._onSessionFocus = () => { if (!document.hidden && this.state.user) this.refreshProfileData(true); };
11662    window.addEventListener('focus', this._onSessionFocus);
11663    if (!publicMode) {
11664      this._marketClockTimer = setInterval(() => {
11665        if (['pool', 'lot'].includes(this.state.route) && !document.hidden) this.updateRoslotMarketClockDom();
11666      }, 1000);
11667      this._auctionStatusTimer = setInterval(() => {
11668        if (this.isRealtimeRoute()) this.syncAuctionStatuses();
11669      }, 4000);
11670      this._liveBidTimer = setInterval(() => {
11671        if (this.isRealtimeRoute()) this.syncLiveBids();
11672      }, 3000);
11673      this._notificationsTimer = setInterval(() => {
11674        if (document.hidden) return;
11675        const active = this.state.notificationsOpen || ['notifications', 'pool'].includes(this.state.route);
11676        if (!active && Date.now() - (this._lastNotificationsFetchAt || 0) < 30000) return;
11677        this.fetchNotifications(true);
11678        if (['notifications', 'pool'].includes(this.state.route)) this.fetchNotificationHistory(true);
11679      }, 5000);
11680    }
11681    this._cabanaCarouselTimer = setInterval(() => {
11682      if (this.state.route === 'cabanaDetail' && Array.isArray(this.state.cabanaLots) && this.state.cabanaLots.length > 1) {
11683        this.startCabanaCarouselCrossfade('*', (previous) => ({
11684          cbCarouselStep: Number(previous.cbCarouselStep || 0) + 1,
11685        }));
11686      }
11687    }, 5000);
11688    this._cabanaTvSyncTimer = setInterval(() => {
11689      if (this.state.route !== 'cabanaTvLower' && this.state.route !== 'cabanaTvConfig') return;
11690      this.syncCabanaTvExternalState();
11691      this.fetchCabanaTvRemoteConfig({ silent: true });
11692      if (this.state.route === 'cabanaTvLower' || this.state.route === 'cabanaTvConfig') {
11693        const now = Date.now();
11694        if (!this._lastCabanaTvCommercialFetchAt || now - this._lastCabanaTvCommercialFetchAt > 2500) {
11695          this._lastCabanaTvCommercialFetchAt = now;
11696          if (this.state.currentCabana) this.refreshCabanaTvCommercial();
11697        }
11698      }
11699      if (this.state.route === 'cabanaTvLower') {
11700        const now = Date.now();
11701        if (Math.floor(Number(this.state.cabanaTvClock || 0) / 1000) !== Math.
11701floor(now / 1000)) this.setState({ cabanaTvClock: now });
11702        if (!this._lastCabanaTvWeatherCheckAt || now - this._lastCabanaTvWeatherCheckAt > 10 * 60 * 1000) {
11703          this._lastCabanaTvWeatherCheckAt = now;
11704          this.fetchCabanaTvWeather(this.state.currentCabana);
11705        }
11706      }
11707    }, 900);
11708    if (!publicMode) { this.bindRemateConfigSorting(); }
11709    this.scheduleDomBindings();
11710  }
11711  componentWillUnmount() {
11712    clearInterval(this._adminVersionTimer);
11713    clearInterval(this._homeChannelTimer);
11714    document.removeEventListener('input', this._onTextInputCapture, true);
11715    document.removeEventListener('compositionstart', this._onFormCompositionStart, true);
11716    document.removeEventListener('compositionend', this._onFormCompositionEnd, true);
11717    if (this._textEntryRenderTimer) clearTimeout(this._textEntryRenderTimer);
11718    Object.values(this._inputRenderTimers || {}).forEach((timer) => clearTimeout(timer));
11719    this.flushCabanaLotViewTimer('unmount');
11720    if (this._authUnsubscribe) {
11721      try { this._authUnsubscribe(); } catch {}
11722      this._authUnsubscribe = null;
11723    }
11724    window.removeEventListener('resize', this._onResize);
11725    document.removeEventListener('touchstart', this._onRoslotTouchStart);
11726    document.removeEventListener('touchend', this._onRoslotTouchEnd);
11727    window.removeEventListener('hashchange', this._onHashChange);
11728    window.removeEventListener('popstate', this._onPopState);
11729    window.removeEventListener('focus', this._onEmailVerificationFocus);
11730    window.removeEventListener('storage', this._onCabanaCommercialStorage);
11731    window.removeEventListener('storage', this._onCabanaTvStorage);
11732    window.removeEventListener('rosgan:tv-config', this._onCabanaTvStorage);
11733    window.removeEventListener('scroll', this._onScroll);
11734    document.removeEventListener('click', this._cabanaAnalyticsClickHandler, true);
11735    document.removeEventListener('visibilitychange', this._onVisibilityChange);
11736    window.removeEventListener('focus', this._onSessionFocus);
11737    document.removeEventListener('pointerdown', this._onRemateSortPointerDown);
11738    window.removeEventListener('pointermove', this._onRemateSortPointerMove);
11739    window.removeEventListener('pointerup', this._onRemateSortPointerUp);
11740    window.removeEventListener('pointercancel', this._onRemateSortPointerUp);
11741    this.destroyCabanaSalesOrderSorting();
11742    if (this._auctionStatusTimer) clearInterval(this._auctionStatusTimer);
11743    if (this._marketClockTimer) clearInterval(this._marketClockTimer);
11744    if (this._liveBidTimer) clearInterval(this._liveBidTimer);
11745    if (this._notificationsTimer) clearInterval(this._notificationsTimer);
11746    if (this._roslotLiveToastTimer) clearTimeout(this._roslotLiveToastTimer);
11747    if (this._roslotLivePulseTimer) clearTimeout(this._roslotLivePulseTimer);
11748    if (this._roslotBidSuccessTimer) clearTimeout(this._roslotBidSuccessTimer);
11749    if (this._cabanaCarouselTimer) clearInterval(this._cabanaCarouselTimer);
11750    if (this._cabanaTvSyncTimer) clearInterval(this._cabanaTvSyncTimer);
11751    if (this._rosganAirtimeTimer) clearInterval(this._rosganAirtimeTimer);
11752    if (this._rosganOpsTimer) clearInterval(this._rosganOpsTimer);
11753    if (this._rosganTvFieldsTimer) clearInterval(this._rosganTvFieldsTimer);
11754    this.resetRosganTvDecks();
11755    this.resetRosganTvMultiview();
11756    if (this._rosganOpsRemoteSaveTimer) clearTimeout(this._rosganOpsRemoteSaveTimer);
11757    if (this._onRosganOpsStorage) {
11758      window.removeEventListener('storage', this._onRosganOpsStorage);
11759      window.removeEventListener('rosgan:remate-operations', this._onRosganOpsStorage);
11760    }
11761    if (this._cabanaTvRemoteSaveTimer) clearTimeout(this._cabanaTvRemoteSaveTimer);
11762    if (this._cabanaPriceBoardTimer) clearInterval(this._cabanaPriceBoardTimer);
11763    if (this._cabanaStatsRefreshTimer) clearInterval(this._cabanaStatsRefreshTimer);
11764    if (this._cabanaPrebidRealtimeTimer) clearInterval(this._cabanaPrebidRealtimeTimer);
11765    if (this._prebidBadgeTimer) clearInterval(this._prebidBadgeTimer);
11766    document.removeEventListener('prebid-started', this._prebidStartHandler);
11767    if (this._creditRealtimeTimer) clearInterval(this._creditRealtimeTimer);
11768    if (this._roslotPresenceTimer) clearInterval(this._roslotPresenceTimer);
11769    if (this._cabanaCommercialPatchTimers) Object.values(this._cabanaCommercialPatchTimers).forEac
11769h((timer) => clearTimeout(timer));
11770    if (this._cabanaLotViewHeartbeat) clearInterval(this._cabanaLotViewHeartbeat);
11771    if (this._cabanaCarouselFadeTimer) clearTimeout(this._cabanaCarouselFadeTimer);
11772    if (this._cabanaCarouselRevealTimer) clearTimeout(this._cabanaCarouselRevealTimer);
11773    if (this._welcomeTimer) clearTimeout(this._welcomeTimer);
11774    if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer);
11775    if (this._welcomeLottiePlayer && this._welcomeLottieComplete) this._welcomeLottiePlayer.removeEventListener('complete', this._welcomeLottieComplete);
11776    if (this._welcomeDotLottie && this._welcomeLottieComplete) this._welcomeDotLottie.removeEventListener('complete', this._welcomeLottieComplete);
11777    if (this._viewT) clearTimeout(this._viewT);
11778    if (this._lotViewPoll) clearInterval(this._lotViewPoll);
11779    if (this._mapPlayT) clearInterval(this._mapPlayT);
11780    if (this._domBindingsFrame) cancelAnimationFrame(this._domBindingsFrame);
11781    if (this._dashRAF) cancelAnimationFrame(this._dashRAF);
11782    if (this._routeWorkTimer) clearTimeout(this._routeWorkTimer);
11783    if (this._revealFallback) clearTimeout(this._revealFallback);
11784    Object.values(this._inputRenderTimers || {}).forEach((timer) => clearTimeout(timer));
11785    this._inputRenderTimers = {};
11786    this._phoneConfirmationResult = null;
11787    this._phoneVerificationId = null;
11788    this.clearPhoneRecaptcha();
11789    this.releaseProfileCrop();
11790    if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview);
11791  }
11792  componentDidUpdate(prevProps, prevState) {
11793    const routeChanged = !!prevState && prevState.route !== this.state.route;
11794    this.syncMailingRoute(prevState);
11795    if (prevState?.user?.uid !== this.state.user?.uid) {
11796      if (this.state.user?.uid) this.refreshRoslotPresence();
11797      else this.setState({ roslotPresence: null, roslotPresenceLoading: false, roslotPresenceError: false });
11798    }
11799    if (prevState && prevState.route === 'lot' && this.state.route !== 'lot') {
11800      if (this._viewT) clearTimeout(this._viewT);
11801      if (this._lotViewPoll) clearInterval(this._lotViewPoll);
11802      this._viewT = null;
11803      this._lotViewPoll = null;
11804    }
11805    if (routeChanged) {
11806      if (this.state.route === 'pool') this.refreshRoslotPresence();
11807      if (this.state.route === 'profile') {
11808        this.animateDash();
11809        this.refreshCreditRealtime({ refreshAuthorizations: true });
11810      }
11811      this.scheduleRouteWork(this.state.route);
11812    }
11813    if (this.state.route === 'roslotSummary'
11814      && this.state.authReady
11815      && this.state.user
11816      && !this.state.roslotDailyReport
11817      && !this.state.roslotDailyReportLoading
11818      && (routeChanged || prevState?.authReady !== this.state.authReady || prevState?.user?.uid !== this.state.user?.uid || prevState?.apiUser !== this.state.apiUser)) {
11819      this.fetchRoslotDailyReport();
11820    }
11821    if (this.state.route === 'lot' && (
11822      routeChanged
11823      || prevState?.user?.uid !== this.state.user?.uid
11824      || prevState?.userConsignataria !== this.state.userConsignataria
11825    )) this.fetchLotSellerConsignataria(this.currentLot());
11826    const activeInput = document.activeElement;
11827    const activeTag = String(activeInput?.tagName || '').toLowerCase();
11828    const activeType = String(activeInput?.type || 'text').toLowerCase();
11829    const typing = activeTag === 'textarea' || (activeTag === 'input' && !['checkbox', 'radio', 'range', 'file', 'date', 'time'].includes(activeType));
11830    if (!typing || routeChanged) this.scheduleDomBindings();
11831    const vp = document.querySelector('[data-lot-player]');
11832    if (vp && vp.dataset.src) {
11833      const source = vp.dataset.src;
11834      if (vp.getAttribute('src') !== source) {
11835        vp.removeAttribute('poster');
11836        vp.src = source;
11837        vp.defaultMuted = true;
11838        vp.muted = true;
11839        try { vp.volume = 0; } catch (_) {}
11840        vp.load();
11841        vp.play().catch(() => {});
11842      }
11843      const currentLot = this.currentLot();
11844      const videoCount = currentLot?.videos?.length || 0;
11845      vp.loop = videoCount <= 1;
11846      this.hydrateVideoThumbnails(currentLot?.videos || []);
11847    }
11848    const rematePlayer = document.querySelector('video[data-remate-lot-player="true"]');
11849    if (rematePlayer && rematePlayer.dataset.src) {
11850      const source = rematePlayer.dataset.src;
11851      const sourceChanged = rematePlayer.getAttribute('src') !== source;
11852      const needsAutoplayInit = rematePlayer.dataset.autoplaySource !== source;
11853      rematePlayer.autoplay = true;
11854      rematePlayer.loop = true;
11855      rematePlayer.playsInline = true;
11856      if (needsAutoplayInit) {
11857        rematePlayer.dataset.autoplaySource = source;
11858        rematePlayer.defaultMuted = true;
11859        rematePlayer.muted = true;
11860        try { rematePlayer.volume = 0; } catch (_) {}
11861        if (sourceChanged) {
11862          rematePlayer.src = source;
11863          rematePlayer.load();
11864        }
11865        rematePlayer.play().catch(() => {});
11866      }
11867    }
11868    if (prevState && (
11869      prevState.route !== this.state.route ||
11870      prevState.cabanaSalesOrderGrouped !== this.state.cabanaSalesOrderGrouped ||
11871      prevState.cabanaSalesOrderLots !== this.state.cabanaSalesOrderLots
11872    )) {
11873      this.destroyCabanaSalesOrderSorting();
11874    }
11875  }
11876  scheduleDomBindings() {
11877    if (this._domBindingsFrame) return;
11878    this._domBindingsFrame = requestAnimationFrame(() => {
11879      this._domBindingsFrame = null;
11880      window.RosganNameFit?.sync();
11881      this.bindReveals();
11882      if (this.state.route === 'home') this.bindDragScroll();
11883      if (this.state.route === 'profile') {
11884        this.relocateSellerBuyerPanel();
11885        this.bindProfileImageErrors();
11886        this.drawProfileCrop();
11887      }
11888      if (['cabanaLotDetail', 'cabanaDetail'].includes(this.state.route)) this.syncCatalogFloatingContact();
11889    });
11890  }
11891  isRealtimeRoute(route = this.state.route) {
11892    return ['home', 'pool', 'lot', 'profile'].includes(route);
11893  }
11894  bindProfileImageErrors() {
11895    if (this.state.profilePhotoFailed || !this.profilePhotoUrl()) return;
11896    document.querySelectorAll('img[data-profile-avatar="true"]').forEach((image) => {
11897      if (image.dataset.profileErrorBound === 'true') return;
11898      image.dataset.profileErrorBound = 'true';
11899      const fail = () => {
11900        if (!this.state.profilePhotoFailed) this.markProfilePhotoFailed();
11901      };
11902      image.addEventListener('error', fail, { once: true });
11903      if (image.complete && image.naturalWidth === 0) fail();
11904    });
11905  }
11906  drawProfileCrop() {
11907    if (!this.state.profileCropOpen || !this._profileCropImage) return;
11908    const canvas = document.querySelector('canvas[data-profile-crop-canvas="true"]');
11909    if (!canvas) return;
11910    const image = this._profileCropImage;
11911    const context = canvas.getContext('2d');
11912    if (!context || !image.naturalWidth || !image.naturalHeight) return;
11913    const size = canvas.width || 512;
11914    const zoom = Math.max(1, Number(this.state.profileCropZoom) || 1);
11915    const scale = Math.max(size / image.naturalWidth, size / image.naturalHeight) * zoom;
11916    const width = image.naturalWidth * scale;
11917    const height = image.naturalHeight * scale;
11918    const x = -(width - size) * Math.max(0, Math.min(100, Number(this.state.profileCropX) || 0)) / 100;
11919    const y = -(height - size) * Math.max(0, Math.min(100, Number(this.state.profileCropY) || 0)) / 100;
11920    context.clearRect(0, 0, size, size);
11921    context.drawImage(image, x, y, width, height);
11922  }
11923  relocateSellerBuyerPanel() {
11924    const buyers = document.getElementById('seller-buyers-panel');
11925    const analytics = document.getElementById('profile-analytics-grid');
11926    if (!buyers || !analytics || buyers.parentNode !== analytics.parentNode || buyers.nextElementSibling === analytics) return;
11927    analytics.insertAdjacentElement('beforebegin', buyers);
11928  }
11929  bindDragScroll() {
11930    document.querySelectorAll('[data-drag-scroll]:not([data-drag-bound])').forEach((el) => {
11931      el.dataset.dragBound = '1';
11932      let down = false, startX = 0, startLeft = 0, moved = 0;
11933      el.addEventListener('pointerdown', (e) => { down = true; moved = 0; startX = e.clientX; startLeft = el.scrollLeft; el.style.cursor = 'grabbing'; el.setPointerCapture?.(e.pointerId); this._carPause = true; });
11934      el.addEventListener('pointermove', (e) => { if (!down) return; const dx = e.clientX - startX; moved = Math.abs(dx); el.scrollLeft = startLeft - dx; });
11935      const up = () => { down = false; el.style.cursor = 'grab'; setTimeout(() => { this._carPause = false; }, 4000); };
11936      el.addEventListener('pointerup', up); el.addEventListener('pointercancel', up); el.addEventListener('pointerleave', up);
11937      el.addEventListener('click', (e) => { if (moved > 6) { e.preventDefault(); e.stopPropagation(); } }, true);
11938      el.addEventListener('mouseenter', () => { this._carHover = true; });
11939      el.addEventListener('mouseleave', () => { this._carHover = false; });
11940      el.addEventListener('scroll', () => {
11941        const card = el.querySelector('[data-drag-item]'); const step = card ?card.offsetWidth + 16 : 300;
11942        const real = this._monthReal || 1;
11943        const idx = ((Math.round(el.scrollLeft / step) % real) + real) % real;
11944        if (idx !== this.state.carSlide) this.setState({ carSlide: idx });
11945        if (this._monthLoop) {
11946          clearTimeout(this._wrapT);
11947          this._wrapT = setTimeout(() => {
11948            if (!this._carCentered) return;
11949            const setW = step * real;
11950            if (setW <= 0) return;
11951            if (el.scrollLeft >= setW * 2) el.scrollLeft -= setW;
11952            else if (el.scrollLeft < setW * 0.5) el.scrollLeft += setW;
11953          }, 140);
11954        }
11955      }, { passive: true });
11956      el.querySelectorAll('[data-drag-item]').forEach((it) => it.addEventListener('dragstart', (e) => e.preventDefault()));
11957      if (this._monthLoop) this.centerCarousel(el);
11958      this.startCarouselAuto();
11959    });
11960  }
11961  centerCarousel(el, tries) {
11962    tries = tries || 0;
11963    const real = this._monthReal || 1;
11964    const card = el.querySelector('[data-drag-item]');
11965    const step = card ?card.offsetWidth + 16 : 300;
11966    const target = step * real;
11967    // esperar a que el layout refleje las 3 copias antes de centrar
11968    if (el.scrollWidth < step * real * 2.5 && tries < 30) { requestAnimationFrame(() => this.centerCarousel(el, tries + 1)); return; }
11969    el.scrollLeft = target;
11970    this._carCentered = true;
11971  }
11972  startCarouselAuto() {
11973    if (this._carTimer || !(this.props.animaciones || true)) return;
11974    this._carTimer = setInterval(() => {
11975      if (this.state.route !== 'home' || this._carPause || this._carHover) return;
11976      const el = document.querySelector('[data-drag-scroll]');
11977      if (!el) return;
11978      const card = el.querySelector('[data-drag-item]');
11979      const step = card ?card.offsetWidth + 16 : 300;
11980      if (!this._monthLoop) {
11981        const maxLeft = el.scrollWidth - el.clientWidth - 4;
11982        if (el.scrollLeft >= maxLeft) { el.scrollTo({ left: 0, behavior: 'smooth' }); return; }
11983      }
11984      el.scrollBy({ left: step, behavior: 'smooth' });
11985    }, 5000);
11986  }
11987  revealEl(el) { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }
11988  bindReveals() {
11989    if (!(this.props.animaciones || true)) { document.querySelectorAll('[data-reveal]').forEach((el) => { el.dataset.bound = '1'; this.revealEl(el); }); return; }
11990    if (!this._io) this._io = new IntersectionObserver((entries) => {
11991      entries.forEach((en) => { if (en.isIntersecting) { this.revealEl(en.target); this._io.unobserve(en.target); } });
11992    }, { root: null, rootMargin: '0px 0px -5% 0px', threshold: 0 });
11993    const fresh = [...document.querySelectorAll('[data-reveal]:not([data-bound])')];
11994    fresh.forEach((el, i) => {
11995      el.dataset.bound = '1';
11996      el.style.opacity = '0'; el.style.transform = 'translateY(12px)';
11997      el.style.transition = 'opacity 280ms cubic-bezier(0.22,0.72,0.18,1), transform 280ms cubic-bezier(0.22,0.72,0.18,1)';
11998      el.style.transitionDelay = this.state.isMobile ?'0ms' : ((i % 5) * 24 + 'ms');
11999      const r = el.getBoundingClientRect();
12000      if (r.top < (window.innerHeight || 800) && r.bottom > 0) { requestAnimationFrame(() => requestAnimationFrame(() => this.revealEl(el))); }
12001      else this._io.observe(el);
12002    });
12003    if (fresh.length) { clearTimeout(this._revealFallback); this._revealFallback = setTimeout(() => document.querySelectorAll('[data-reveal][data-bound]').forEach((el) => this.revealEl(el)), 360); }
12004  }
12005  animateStats() {
12006    if (this.state.statProgress !== 1) this.setState({ statProgress: 1 });
12007  }
12008  animateDash() {
12009    if (this._dashRAF) cancelAnimationFrame(this._dashRAF);
12010    this._dashRAF = null;
12011    if (this.state.dashProgress !== 1) this.setState({ dashProgress: 1 });
12012  }
12013  seededSeries(seed, n, lo, hi) {
12014    const out = []; let x = seed * 9301 + 49297;
12015    for (let i = 0; i < n; i++) { x = (x * 9301 + 49297) % 233280; out.push(lo + (x / 233280) * (hi - lo)); }
12016    return out;
12017  }
12018  linePath(vals, w, h, pad) {
12019    const max = Math.max(...vals), min = Math.min(...vals), rng = (max - min) || 1, n = vals.length;
12020    return vals.map((v, i) => { const x = pad + i * ((w - 2 * pad) / (n - 1)); const y = h - pad - ((v - min) / rng) * (h - 2 * pad); return (i ?'L' : 'M') + x.toFixed(1) + ' ' + y.toFixed(1); }).join(' ');
12021  }
12022  areaPath(vals, w, h, pad) { return this.linePath(vals, w, h, pad) + ' L ' + (w - pad).toFixed(1) + ' ' + (h - pad
12022) + ' L ' + pad + ' ' + (h - pad) + ' Z'; }
12023  async fetchMavData(force = false) {
12024    const date = this.state.mavDate;
12025    if (!date || (this.state.mavLoading && !force)) return;
12026    this.setState({ mavLoading: true, mavError: '' });
12027    try {
12028      const query = '?fecha=' + encodeURIComponent(date);
12029      const [summaryResponse, rangesResponse] = await Promise.all([
12030        fetch('/api/mav/resumen-diario' + query),
12031        fetch('/api/mav/segregacion-rangos-segmentos' + query),
12032      ]);
12033      if (!summaryResponse.ok || !rangesResponse.ok) throw new Error('El proveedor no devolvió información para la fecha seleccionada.');
12034      const [summary, ranges] = await Promise.all([summaryResponse.json(), rangesResponse.json()]);
12035      const rangeRows = Array.isArray(ranges?.resultados) ?ranges.resultados : [];
12036      if (!summary || !rangeRows.length) throw new Error('No hay operaciones informadas para la fecha seleccionada.');
12037      this.setState({ mavSummary: summary, mavRanges: rangeRows, mavLoading: false, mavError: '' });
12038    } catch (error) {
12039      this.setState({ mavSummary: null, mavRanges: null, mavLoading: false, mavError: error.message || 'No se pudieron obtener los datos del MAV.' });
12040    }
12041  }
12042  mavData() {
12043    const s = this.state;
12044    const summary = s.mavSummary || {};
12045    const rows = Object.values(summary).reduce((all, value) => Array.isArray(value) ?all.concat(value) : all, []);
12046    const number = (value) => Number(value) || 0;
12047    const total = rows.reduce((sum, row) => sum + number(row.monto_total_negociado), 0);
12048    const previousTotal = rows.reduce((sum, row) => sum + number(row.monto_total_negociado_dia_anterior), 0);
12049    const weighted = (field) => total ?rows.reduce((sum, row) => sum + number(row.monto_total_negociado) * number(row[field]), 0) / total : 0;
12050    const amountDelta = previousTotal ?((total - previousTotal) / previousTotal) * 100 : 0;
12051    const money = (value) => {
12052      const n = number(value);
12053      if (n >= 1e9) return '$' + (n / 1e9).toLocaleString('es-AR', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' mil M';
12054      if (n >= 1e6) return '$' + (n / 1e6).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + ' M';
12055      return '$' + Math.round(n).toLocaleString('es-AR');
12056    };
12057    const rate = (value) => number(value).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '%';
12058    const deltaText = amountDelta ?Math.abs(amountDelta).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '% vs. rueda anterior' : '';
12059    const ranges = ['0-30', '31-60', '61-90', '91-120', '121-180', '181-365'];
12060    const metric = s.mavMetric || 'tasa_prom_pond';
12061    const segmentDefs = [
12062      { key: 'avalado', label: 'Avalado', color: '#075ca8' },
12063      { key: 'garantizado', label: 'Garantizado', color: '#16856f' },
12064      { key: 'no garantizado', label: 'No garantizado', color: '#e84655' },
12065    ];
12066    const sourceRows = Array.isArray(s.mavRanges) ?s.mavRanges : [];
12067    const rawSeries = segmentDefs.map((definition) => {
12068      const source = sourceRows.find((row) => (row.segmento || '').toLowerCase() === definition.key);
12069      const byRange = new Map((source?.datos || []).map((item) => [item.rango, item]));
12070      const records = ranges.map((rangeName) => byRange.get(rangeName) || null);
12071      return { ...definition, records, values: records.map((record) => record ?number(record[metric]) : null) };
12072    });
12073    const allValues = rawSeries.flatMap((series) => series.values.filter((value) => value !== null));
12074    const W = 940, H = 286, left = 58, right = 46, top = 24, bottom = 42;
12075    const rawMin = allValues.length ?Math.min(...allValues) : 0;
12076    const rawMax = allValues.length ?Math.max(...allValues) : 1;
12077    const padding = Math.max((rawMax - rawMin) * 0.14, 1);
12078    const min = Math.max(0, rawMin - padding), max = rawMax + padding, span = (max - min) || 1;
12079    const x = (index) => left + index * ((W - left - right) / (ranges.length - 1));
12080    const y = (value) => top + ((max - value) / span) * (H - top - bottom);
12081    const mavSeries = rawSeries.map((series) => {
12082      const points = series.values.map((value, index) => {
12083        if (value === null) return null;
12084        const record = series.records[index] || {};
12085        const point = { x: x(index), y: y(value), range: ranges[index] + ' días', value: rate(value), segment: series.label, color: series.color };
12086        const hover = {
12087          x: point.x, y: point.y, range: point.range, value: point.value, segment: point.segment, color: point.color,
12088          tna: rate(record.tasa_prom_pond), tea: rate(record.tasa_tea_pond), tem: rate(record.tasa_tem_pond),
12089        };
12090        point.aria = point.segment + ', ' + point.range + ', ' + point.value;
12091        point.show = () => this.setState({ mavHover: hover });
12092        point.toggle = () => this.setState({ mavHover: s.mavHover?.segment === hover.segment && s.mavHover?.range === hover.range ?null : hover });
12093        return point;
12094      }
12094).filter(Boolean);
12095      return { ...series, points, path: points.map((point, index) => (index ?'L' : 'M') + point.x.toFixed(1) + ' ' + point.y.toFixed(1)).join(' ') };
12096    });
12097    const dateObj = s.mavDate ?new Date(s.mavDate + 'T12:00:00') : null;
12098    const hover = s.mavHover;
12099    const tooltipX = hover ?Math.max(8, Math.min(W - 192, hover.x > W * 0.72 ?hover.x - 194 : hover.x + 12)) : 0;
12100    const tooltipY = hover ?Math.max(8, Math.min(H - 62, hover.y - 62)) : 0;
12101    const tooltipLeft = hover ?Math.max(17, Math.min(83, (hover.x / W) * 100)) : 50;
12102    const tooltipTop = hover ?(hover.y / H) * 100 : 50;
12103    return {
12104      mavDate: s.mavDate,
12105      mavToday: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 10),
12106      mavDateSet: (event) => this.setState({ mavDate: event.target.value, mavHover: null }, () => this.fetchMavData(true)),
12107      mavRetry: () => this.fetchMavData(true),
12108      mavLoading: !!s.mavLoading,
12109      mavHasError: !s.mavLoading && !!s.mavError,
12110      mavError: s.mavError,
12111      mavReady: !s.mavLoading && !s.mavError && !!s.mavSummary && !!s.mavRanges,
12112      mavSkeletons: [{}, {}, {}, {}],
12113      mavKpis: [
12114        { label: 'Volumen negociado', value: money(total), delta: deltaText, deltaUp: amountDelta > 0, deltaDown: amountDelta < 0, deltaColor: amountDelta >= 0 ?'#16856f' : '#e84655' },
12115        { label: 'TNA promedio', value: rate(weighted('tasa_prom_pond')), delta: 'Ponderada por volumen', deltaColor: '#74859a' },
12116        { label: 'TEA promedio', value: rate(weighted('tasa_tea_ponderada')), delta: 'Tasa efectiva anual', deltaColor: '#74859a' },
12117        { label: 'TEM promedio', value: rate(weighted('tasa_tem_ponderada')), delta: 'Tasa efectiva mensual', deltaColor: '#74859a' },
12118      ],
12119      mavMetricTabs: [
12120        { key: 'tasa_prom_pond', label: 'TNA' },
12121        { key: 'tasa_tea_pond', label: 'TEA' },
12122        { key: 'tasa_tem_pond', label: 'TEM' },
12123      ].map((item) => {
12124        const active = metric === item.key;
12125        return { label: item.label, pick: () => this.setState({ mavMetric: item.key, mavHover: null }), bg: active ?'#003b5c' : '#fff', color: active ?'#fff' : '#52657a', border: active ?'#003b5c' : '#d8e3ed' };
12126      }),
12127      mavW: W, mavH: H, mavSeries,
12128      mavXTicks: ranges.map((label, index) => ({ x: x(index), label: label + ' días' })),
12129      mavYTicks: [0, 0.25, 0.5, 0.75, 1].map((step) => { const value = min + (max - min) * step; return { y: y(value), label: rate(value) }; }),
12130      mavDateLabel: dateObj ?dateObj.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' }) : '',
12131      mavTooltipVisible: !!hover,
12132      mavTooltipHide: () => this.setState({ mavHover: null }),
12133      mavTooltipX: tooltipX, mavTooltipY: tooltipY,
12134      mavTooltipDotX: tooltipX + 13, mavTooltipDotY: tooltipY + 18,
12135      mavTooltipTextX: tooltipX + 23, mavTooltipTitleY: tooltipY + 21, mavTooltipValueY: tooltipY + 40,
12136      mavTooltipColor: hover?.color || '#075ca8',
12137      mavTooltipTitle: hover ?hover.segment + ' · ' + hover.range : '',
12138      mavTooltipValue: hover ?({ tasa_prom_pond: 'TNA', tasa_tea_pond: 'TEA', tasa_tem_pond: 'TEM' }[metric] || 'Tasa') + ' ' + hover.value : '',
12139      mavTooltipLeft: tooltipLeft,
12140      mavTooltipTop: tooltipTop,
12141      mavTooltipTransform: hover && hover.y < 105 ?'translate(-50%, 16px)' : 'translate(-50%, calc(-100% - 14px))',
12142      mavTooltipRates: hover ? [
12143        { label: 'TNA', value: hover.tna },
12144        { label: 'TEA', value: hover.tea },
12145        { label: 'TEM', value: hover.tem },
12146      ] : [],
12147    };
12148  }
12149  mercadoData() {
12150    const arr = this.state.indices || [];
12151    const s = this.state;
12152    const defs = [
12153      { key: 'piri', label: 'PIRI', dec: 2, unit: '$/kg', compact: false, desc: 'Invernada' },
12154      { key: 'pirc', label: 'PIRC', dec: 0, unit: '$/vientre', compact: true, desc: 'Cría' },
12155      { key: 'indice_ternero', label: 'Índice Ternero', dec: 2, unit: '$/kg', compact: false, desc: 'Ternero' },
12156    ];
12157    const active = defs.find((d) => d.key === s.mercIndex) || defs[0];
12158    const fullSeries = this.indexSeries(active.key);
12159    const range = [6,12,24].includes(s.mercRange) ? s.mercRange : 12;
12160    const series = range >= 999 ?fullSeries : fullSeries.slice(-range);
12161    const vals = series.map((p) => p.v);
12162    const W = 920, H = 300, PAD = 46;
12163    const max = Math.max(...vals, 1), min = Math.min(...vals, 0);
12164
12165    const n = vals.length;
12166    const px = (i) => PAD + (n <= 1 ?0 : (i / (n - 1)) * (W - 2 * PAD));
12167    const py = (v) => { const rng = (max - min) || 1; return H - PAD - ((v - min) / rng) * (H - 2 * PAD); };
12168    const pts = series.map((p, i) => ({ x: px(i), y: py(p.v), left: (px(i)/W*100)+'%', top: (py(p.v)/H*100)+'%', focus: event => event.currentTarget.focus(), v: p.v, label: p.label, vFmt: active.compact ?this.fmtCompact(p.v) : this.fmtMoney(p.v, active.dec) }));
12169    const line = pts.map((p,i)=>(i?'L':'M')+p.x+' '+p.y).join(' ');
12170    const area = n > 1 ? line+' L'+pts[n-1].x+' '+(H-PAD)+' L'+pts[0].x+' '+(H-PAD)+' Z' : '';
12171    const last = series[n - 1], first = series[0];
12172    const cur = last ?last.v : 0, prev = n > 1 ?series[n - 2].v : 0;
12173    const pct = prev ?((cur - prev) / prev) * 100 : 0;
12174    const yoy = first ?((cur - first.v) / first.v) * 100 : 0;
12175    const avg = vals.length ?vals.reduce((a, b) => a + b, 0) / vals.length : 0;
12176    const gEvery = Math.ceil(n / 8) || 1;
12177    const xLabels = series.map((p, i) => ({ x: px(i), left: (px(i)/W*100)+'%', label: p.label, show: i % gEvery === 0 || i === n - 1 }));
12178    const yTicks = [0, 0.25, 0.5, 0.75, 1].map((t) => { const v = min + t * (max - min); return { y: py(v), top: (py(v)/H*100)+'%', label: active.compact ?this.fmtCompact(v) : this.fmtMoney(v, 0) }; });
12179    const latest = arr[0];
12180    const tipos = latest?.tipos || [];
12181    const tipoObj = tipos.find((t) => t.titulo === s.mercTipo) || tipos[0];
12182    const cats = (tipoObj?.categorias || []).filter((c) => (c.razas || []).some((r) => r.precio > 0) || c.precio > 0);
12183    const razaSet = [];
12184    cats.forEach((c) => (c.razas || []).forEach((r) => { if (!razaSet.includes(r.titulo)) razaSet.push(r.titulo); }));
12185    const allPrices = [];
12186    cats.forEach((c) => (c.razas || []).forEach((r) => { if (r.precio > 0) allPrices.push(r.precio); }));
12187    const hMax = Math.max(...allPrices, 1), hMin = Math.min(...allPrices, 0);
12188    const heatColor = (v) => { if (!v) return 'rgba(148,163,184,0.10)'; const t = (v - hMin) / ((hMax - hMin) || 1); const l = 92 - t * 52; return 'hsl(155 62% ' + l + '%)'; };
12189    const heatText = (v) => { if (!v) return '#b6c2d1'; const t = (v - hMin) / ((hMax - hMin) || 1); return t > 0.55 ?'#fff' : '#0d3a2a'; };
12190    const heatRows = cats.map((c) => ({
12191      cat: c.titulo,
12192      cells: razaSet.map((rz) => { const r = (c.razas || []).find((x) => x.titulo === rz); const v = r ?r.precio : 0; return { v, bg: heatColor(v), color: heatText(v), fmt: v ?(active.compact ?this.fmtCompact(v) : '$' + Math.round(v).toLocaleString('es-AR')) : '—' }; }),
12193      promFmt: c.precio ?(active.compact ?this.fmtCompact(c.precio) : '$' + Math.round(c.precio).toLocaleString('es-AR')) : '—',
12194    }));
12195    return {
12196      mercTabs: defs.map((d) => ({ label: d.label, desc: d.desc, active: d.key === active.key, pick: () => this.setState({ mercIndex: d.key }), bg: d.key === active.key ?'#002e5f' : '#fff', color: d.key === active.key ?'#fff' : '#3d4c5f', border: d.key === active.key ?'#002e5f' : '#dde7f2' })),
12197      mercActive: active.label, mercUnit: active.unit,
12198      mercHasData: n > 1, mercW: W, mercH: H, mercLine: line, mercArea: area,
12199      mercPts: pts, mercXLabels: xLabels, mercYTicks: yTicks,
12200      mercCur: active.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, active.dec),
12201      mercRollEl: this.rollEl(active.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, active.dec)),
12202      mercDrawKey: active.key + '-' + range + '-' + n,
12203      mercPct: (pct > 0 ?'+' : '') + pct.toFixed(1) + '%', mercPctColor: pct >= 0 ?'#1fbf75' : '#ff5a6a',
12204      mercPctArrow: pct >= 0 ?'M6 14l6-6 6 6' : 'M6 10l6 6 6-6',
12205      mercYoy: (yoy > 0 ?'+' : '') + yoy.toFixed(1) + '%', mercYoyColor: yoy >= 0 ?'#1fbf75' : '#ff5a6a',
12206      mercMax: active.compact ?this.fmtCompact(max) : this.fmtMoney(max, active.dec),
12207      mercMin: active.compact ?this.fmtCompact(min) : this.fmtMoney(min, active.dec),
12208      mercAvg: active.compact ?this.fmtCompact(avg) : this.fmtMoney(avg, active.dec),
12209      mercPeriod: series.length ?(first.label + ' · ' + last.label) : '',
12210      mercMonthLabel: latest ?this.MES_ES[(latest.mes_remate || 1) - 1] + ' ' + latest.anio_remate : '',
12211      mercTipoTabs: tipos.map((t) => ({ label: t.titulo, active: t.titulo === (tipoObj?.titulo), pick: () => this.setState({ mercTipo: t.titulo }), bg: t.titulo === (tipoObj?.titulo) ?'#1f7a63' : '#fff', color: t.titulo === (tipoObj?.titulo) ?'#fff' : '#3d4c5f', border: t.titulo === (tipoObj?.titulo) ?'#1f7a63' : '#dde7f2' })),
12212      mercRazas: razaSet, heatRows,
12213      mercRangeTabs: [{ n: 6, l: '6 meses' }, { n: 12, l: '12 meses' }, { n: 24, l: '24 meses' }].map((o) => { const on = (s.mercRange || 12) === o.n; return { label: o.l, pick: () => this.setState({ mercRange: o.n }), chipBg: on ?'#2fe08d' : 'rgba(255,255,255,0.09)', chipColor: on ?'#04213f' : '#c8d8ea' }; }),
12214      mercNoData: !arr.length,
12215    };
12216  }
12217  normProv(v) {
12218    const s = (v || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z ]/g, ' ').replace(/\s+/g, ' ').trim();
12219    if (!s) return '';
12220    if ((s.includes('ciudad') && s.includes('buenos aires')) || s === 'caba' || s === 'cap' || s === 'capital' || s.includes('capital federal') || s.includes('distrito federal')) return 'caba';
12221    if (s.includes('tierra del fuego') || s === 'tf') return 'tierra del fuego';
12222    const codes = {
12223      ba: 'buenos aires', cat: 'catamarca', cba: 'cordoba', cor: 'cordoba', cord: 'cordoba', cts: 'corrientes', ctes: 'corrientes', corr: 'corrientes',
12224      cho: 'chaco', chc: 'chaco', chu: 'chubut', chb: 'chubut', chub: 'chubut', er: 'entre rios', ers: 'entre rios', f: 'formosa', for: 'formosa', fsa: 'formosa',
12225      juj: 'jujuy', juy: 'jujuy', jy: 'jujuy', lp: 'la pampa', lr: 'la rioja', mza: 'mend
12225oza', men: 'mendoza', mdz: 'mendoza', mis: 'misiones', mnes: 'misiones',
12226      nqn: 'neuquen', neu: 'neuquen', rn: 'rio negro', rng: 'rio negro', sal: 'salta', sj: 'san juan', sl: 'san luis', sc: 'santa cruz', scz: 'santa cruz',
12227      sf: 'santa fe', sfe: 'santa fe', se: 'santiago del estero', sde: 'santiago del estero', sgo: 'santiago del estero', tuc: 'tucuman', tm: 'tucuman',
12228    };
12229    if (codes[s]) return codes[s];
12230    return s;
12231  }
12232  buildGeo(geo) {
12233    const polys = []; let minX = 1e18, minY = 1e18, maxX = -1e18, maxY = -1e18;
12234    geo.features.forEach((f) => {
12235      const g = f.geometry; if (!g) return;
12236      const mp = g.type === 'Polygon' ?[g.coordinates] : (g.type === 'MultiPolygon' ?g.coordinates : []);
12237      mp.forEach((poly) => poly.forEach((ring) => ring.forEach((pt) => { const x = pt[0], y = pt[1]; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; })));
12238      polys.push({ name: (f.properties && f.properties.name) || '', mp });
12239    });
12240    const W = 560, H = 760, P = 12;
12241    const k = Math.min((W - 2 * P) / ((maxX - minX) || 1), (H - 2 * P) / ((maxY - minY) || 1));
12242    const ox = (W - (maxX - minX) * k) / 2, oy = (H - (maxY - minY) * k) / 2;
12243    const X = (x) => ox + (x - minX) * k, Y = (y) => H - oy - (y - minY) * k;
12244    this._geoBox = { minX, minY, maxX, maxY, k, ox, oy, W, H };
12245    this._geoT = (geo['hc-transform'] && geo['hc-transform']['default']) || null;
12246    this._geoProvs = polys.filter((p) => p.name).map((p) => {
12247      let d = '';
12248      p.mp.forEach((poly) => poly.forEach((ring) => {
12249        if (ring.length < 3) return;
12250        d += 'M' + ring.map((pt) => X(pt[0]).toFixed(1) + ' ' + Y(pt[1]).toFixed(1)).join('L') + 'Z';
12251      }));
12252      return { key: this.normProv(p.name), name: p.name, d };
12253    });
12254  }
12255  buildAgg(list) {
12256    const agg = {}, catCount = {}, pts = {}, monthsByYear = {};
12257    const bump = (y, c, p, pk, pc, heads, loc, cons) => {
12258      const a = ((agg[y] = agg[y] || {})[c] = agg[y][c] || {});
12259      const cell = (a[p] = a[p] || { sk: 0, nk: 0, sc: 0, nc: 0, lots: 0, heads: 0, locs: {}, cons: {} });
12260      if (pk > 0) { cell.sk += pk; cell.nk++; }
12261      if (pc > 0) { cell.sc += pc; cell.nc++; }
12262      cell.lots++; cell.heads += heads;
12263      if (loc) { const L = (cell.locs[loc] = cell.locs[loc] || { sk: 0, nk: 0, sc: 0, nc: 0, lots: 0 }); if (pk > 0) { L.sk += pk; L.nk++; } if (pc > 0) { L.sc += pc; L.nc++; } L.lots++; }
12264      if (cons) { const C = (cell.cons[cons] = cell.cons[cons] || { sk: 0, nk: 0, sc: 0, nc: 0, lots: 0, heads: 0 }); if (pk > 0) { C.sk += pk; C.nk++; } if (pc > 0) { C.sc += pc; C.nc++; } C.lots++; C.heads += heads; }
12265    };
12266    (list || []).forEach((r) => {
12267      const d = new Date(r.fecha_remate || r.fecha || '');
12268      const y = isNaN(d) ?null : d.getFullYear(); if (!y || y < 2000) return;
12269      const m = d.getMonth() + 1;
12270      const ym = y * 100 + m;
12271      const p = this.normProv(r.nombre_provincia || r.provincia); if (!p) return;
12272      const cat = ((r.nombre_categoria || r.categoria || '') + '').trim() || 'Sin categoría';
12273      const pk = +(r.precio_kilo_conciliado || r.precio_kilo || r.precio_kilo_remate || 0) || 0;
12274      const pc = +(r.precio_cabeza_conciliado || r.precio_cabeza || r.precio_cabeza_remate || 0) || 0;
12275      if (!pk && !pc) return;
12276      const heads = +(r.cantidad || 0) || 0;
12277      const loc = ((r.nombre_localidad || r.localidad || '') + '').trim();
12278      const cons = ((r.consignataria || r.nombre_cuenta || r.nombre_consignatario || '') + '').trim();
12279      catCount[cat] = (catCount[cat] || 0) + 1;
12280      (monthsByYear[y] = monthsByYear[y] || {})[m] = true;
12281      bump(y, cat, p, pk, pc, heads, loc, cons); bump(y, '__all__', p, pk, pc, heads, loc, cons);
12282      bump(ym, cat, p, pk, pc, heads, loc, cons); bump(ym, '__all__', p, pk, pc, heads, loc, cons);
12283      let lat = +r.latitud, lng = +r.longitud;
12284      if ((!lat || !lng) && typeof r.latlong === 'string' && r.latlong.includes(',')) { const ll = r.latlong.split(','); lat = +ll[0]; lng = +ll[1]; }
12285      if (lat < -20 && lat > -56 && lng < -52 && lng > -76) {
12286        [y, ym].forEach((per) => [cat, '__all__'].forEach((c3) => {
12287          const P2 = ((pts[per] = pts[per] || {})[c3] = pts[per][c3] || {});
12288          const kk = p + '|' + (loc || 's/l');
12289          const o = (P2[kk] = P2[kk] || { name: loc || 'Sin localidad', prov: p, latS: 0, lngS: 0, n: 0, sk: 0, nk: 0, sc: 0, nc: 0, lots: 0 });
12290          o.latS += lat; o.lngS += lng; o.n++; if (pk > 0) { o.sk += pk; o.nk++; } if (pc > 0) { o.sc += pc; o.nc++; } o.lots++;
12291        }));
12292      }
12293    });
12294    this._mapPts = pts;
12295    this._mapAgg = agg;
12296    this._mapYears = Object.keys(agg).map(Number).filter((k) => k < 10000).sort((a, b) => a - b);
12297    this._monthsByYear = {};
12298    Object.keys(monthsByYear).forEach((y2) => { this._monthsByYear[y2] = Object.keys(monthsByYear[y2]).map(Number).sort((a, b) => a - b); });
12299    this._mapCats = Object.keys(catCount).sort((a, b) => catCount[b] - catCount[a]).slice(0, 8);
12300  }
12301  heatColor(t) {
12302    t = Math.max(0, Math.min(1, t));
12303    const stops = [[38, 128, 196], [96, 178, 128], [240, 168, 48], [225, 45, 58]];
12304    const f = t * (stops.length - 1); const i = Math.min(stops.length - 2, Math.floor(f)); const lt = f - i;
12305    const a = stops[i], b = stops[i + 1];
12306    return 'rgb(' + Math.round(a[0] + (b[0] - a[0]) * lt) + ',' + Math.round(a[1] + (b[1] - a[1]) * lt) + ',' + Math.round(a[2] + (b[2] - a[2]) * lt) + ')';
12307  }
12308  bestMapYear() {
12309    const agg = this._mapAgg || {}; let best = null, bestLots = -1;
12310    Object.keys(agg).forEach((y) => { const cells = agg[y]['__all__'] || {}; let lots = 0; Object.values(cells).forEach((c) => { lots += c.lots || 0; }); if (lots > bestLots) { bestLots = lots; best = +y; } });
12311    return best;
12312  }
12313  async fetchMapa() {
12314    const url = '/api/preciosporlote';
12315    let geo = await this.unwrap('https://cdn.jsdelivr.net/npm/@highcharts/map-collection@2/countries/ar/ar-all.geo.json');
12316    if (!geo || !geo.features) geo = await this.unwrap('https://code.highcharts.com/mapdata/countries/ar/ar-all.geo.json');
12317    if (geo && geo.features) this.buildGeo(geo);
12318    // The range endpoint is slow, so pull year-by-year (priced years first) and
12319    // render progressively as each batch lands.
12320    const y = new Date().getFullYear();
12321    const years = [y, y - 1, y - 2, y - 3, y - 4];
12322    this._mapTargetYears = years.slice().sort((a, b) => a - b);
12323    this._rawRows = [];
12324    let anyReal = false;
12325    this.setState({ mapFetching: true });
12326    for (let i = 0; i < years.length; i++) {
12327      const yr = years[i];
12328      let offset = 0;
12329      for (let pg = 0; pg < 5; pg++) {
12330        let d = null;
12331        try { d = await this.unwrap(url + '?from=' + yr + '-01-01&to=' + yr + '-12-31&limit=2500&offset=' + offset); } catch (e) { console.warn('precios ' + yr, e); }
12332        const items = d && Array.isArray(d.items) ?d.items : (Array.isArray(d) ?d : null);
12333        if (!items || !items.length) break;
12334        for (let k = 0; k < items.length; k++) this._rawRows.push(items[k]);
12335        this.buildAgg(this._rawRows);
12336        this.calibrateProj();
12337        if (this._mapYears && this._mapYears.length) {
12338          anyReal = true;
12339          const patch = { mapReady: true, mapDemo: false };
12340          if (!this._userPickedYear && !this._mapYearInit) { patch.mapYear = this._mapYears[this._mapYears.length - 1]; this._mapYearInit = true; }
12341          this.setState(patch);
12342        }
12343        if (d.hasMore === false) break;
12344        offset = d.nextOffset != null ?d.nextOffset : offset + items.length;
12345      }
12346    }
12347    if (!anyReal) this.setState({ mapReady: !!this._geoProvs, mapDemo: false });
12348    this.setState({ mapFetching: false });
12349  }
12350  toggleMapPlay() {
12351    this._userPickedYear = true;
12352    if (this._mapPlayT) { clearInterval(this._mapPlayT); this._mapPlayT = null; this.forceUpdate(); return; }
12353    const year = this.state.mapYear;
12354    const months = (this._monthsByYear || {})[year] || [];
12355    if (months.length >= 2) {
12356      let i = months.indexOf(this.state.mapMonth);
12357      const step = () => { i = (i + 1) % months.length; this.setState({ mapMonth: months[i] }); };
12358      this._mapPlayT = setInterval(step, 1300);
12359      step();
12360      return;
12361    }
12362    const years = this._mapYears || []; if (years.length < 2) return;
12363    const step = () => { const i = years.indexOf(this.state.mapYear); this.setState({ mapYear: years[(i + 1) % years.length], mapMonth: null }); };
12364    this._mapPlayT = setInterval(step, 1500);
12365    step();
12366  }
12367  mapTipMove(e, name, txt) {
12368    const svg = e.currentTarget.ownerSVGElement; if (!svg || !svg.parentElement) return;
12369    const host = svg.parentElement.getBoundingClientRect();
12370    this.setState({ mapTip: { x: e.clientX - host.left, y: e.clientY - host.top, name, txt } });
12371  }
12372  calibrateProj() {
12373    this._projOk = false; this._hasPts = false;
12374    if (!this._geoBox || !this._mapPts) return;
12375    const sample = [];
12376    Object.values(this._mapPts).forEach((byCat) => { const all = byCat['__all__'] || {}; Object.values(all).forEach((o) => { if (sample.length < 300) sample.push({ lat: o.latS / o.n, lng: o.lngS / o.n }); }); });
12377    if (!sample.length) return;
12378    this._hasPts = true;
12379    const b = this._geoBox, mx = (b.maxX - b.minX) * 0.03, my = (b.maxY - b.minY) * 0.03;
12380    const inB = (c) => c && isFinite(c[0]) && isFinite(c[1]) && c[0] >= b.minX - mx && c[0] <= b.maxX + mx && c[1] >= b.minY - my && c[1] <= b.maxY + my;
12381    let a = 0, f = 0;
12382    sample.forEach((p2) => { if (inB(this.projRaw(p2.lat, p2.lng, false))) a++; if (inB(this.projRaw(p2.lat, p2.lng, true))) f++; });
12383    this._projFlip = f > a;
12384    this._projOk = Math.max(a, f) >= sample.length * 0.5;
12385  }
12386  projRaw(lat, lng, flip) {
12387    const t = this._geoT;
12388    try {
12389      if (t && t.crs && window.proj4) {
12390        const p = window.proj4(t.crs).forward([lng, lat]);
12391        const res = (t.scale || 1) * (t.jsonres || 1);
12392        const x = (p[0] - (t.xoffset || 0)) * res + (t.jsonmarginX || 0);
12393        const y = flip ?((t.yoffset || 0) - p[1]) * res - (t.jsonmarginY || 0) : (p[1] - (t.yoffset || 0)) * res + (t.jsonmarginY || 0);
12394        return [x, y];
12395      }
12396    } catch (e) {}
12397    const b = this._geoBox;
12398    return [b.minX + ((lng + 73.6) / 20.1) * (b.maxX - b.minX), b.minY + ((lat + 55.1) / 33.4) * (b.maxY - b.minY)];
12399  }
12400  geoSvgXY(x, y) { const b = this._geoBox; return [b.ox + (x - b.minX) * b.k, b.H - b.oy - (y - b.minY) * b.k]; }
12401  mapaData() {
12402    const s = this.state, agg = this._mapAgg, provs = this._geoProvs;
12403    if (!s.mapReady || !agg || !provs) return { mapReady: false, mapLoading: true, mapFetching: true, mapDemo: !!s.mapDemo, mapYearLabel: '', mapMetricLabel: 'promedio $/kg', mapMetricTabs: [], mapCatTabs: [] };
12404    const years = this._mapYears;
12405    const year = years.includes(s.mapYear) ?s.mapYear : years[years.length - 1];
12406    const cat = s.mapCat || '__all__';
12407    const metric = s.mapMetric || 'kg';
12408    const monthsY = (this._monthsByYear || {})[year] || [];
12409    const month = s.mapMonth || null;
12410    const useMonth = !!(month && monthsY.includes(month));
12411    const periodKey = useMonth ?year * 100 + month : year;
12412    const byProv = (agg[periodKey] || {})[cat] || {};
12413    const val = (c) => !c ?0 : (metric === 'kg' ?(c.nk ?c.sk / c.nk : 0) : (c.nc ?c.sc / c.nc : 0));
12414    const fmtV = (v) => metric === 'kg' ?this.fmtMoney(v, 0) : this.fmtCompact(v);
12415    const present = provs.map((p) => ({ p, c: byProv[p.key], v: val(byProv[p.key]) })).filter((x) => x.v > 0);
12416    const vMin = present.length ?Math.min.apply(null, present.map((x) => x.v)) : 0;
12417    const vMax = present.length ?Math.max.apply(null, present.map((x) => x.v)) : 0;
12418    const tt = (v) => vMax > vMin ?(v - vMin) / (vMax - vMin) : 0.5;
12419    const col = (v) => v ?'hsl(211 64% ' + (90 - tt(v) * 61).toFixed(1) + '%)' : '#edf2f8';
12420    const sel = s.mapSel;
12421    const unit = metric === 'kg' ?' $/kg' : ' $/cab';
12422    const view = (s.mapView === 'loc' && this._projOk) ?'loc' : 'prov';
12423    const ptsMap = ((this._mapPts || {})[periodKey] || {})[cat] || {};
12424    const yearPts = ((this._mapPts || {})[year] || {})[cat] || {};
12425    const dotsRaw = view === 'loc' ? Object.values(ptsMap).map((o) => {
12426      const v = metric === 'kg' ?(o.nk ?o.sk / o.nk : 0) : (o.nc ?o.sc / o.nc : 0);
12427      const raw = this.projRaw(o.latS / o.n, o.lngS / o.n, this._projFlip);
12428      const c2 = this.geoSvgXY(raw[0], raw[1]);
12429      return { o, v, cx: c2[0], cy: c2[1] };
12430    }).filter((d2) => d2.v > 0 && d2.cx >= 0 && d2.cx <= 560 && d2.cy >= 0 && d2.cy <= 760) : [];
12431    const heatUnion = view === 'loc' ? Object.keys(yearPts).map((kk) => {
12432      const base = yearPts[kk];
12433      const raw = this.projRaw(base.latS / base.n, base.lngS / base.n, this._projFlip);
12434      const c2 = this.geoSvgXY(raw[0], raw[1]);
12435      const cell = ptsMap[kk];
12436      const v = cell ?(metric === 'kg' ?(cell.nk ?cell.sk / cell.nk : 0) : (cell.nc ?cell.sc / cell.nc : 0)) : 0;
12437      return { kk, cx: c2[0], cy: c2[1], v, lots: cell ?cell.lots : 0, name: base.name, prov: base.prov };
12438    }).filter((d2) => d2.cx >= 0 && d2.cx <= 560 && d2.cy >= 0 && d2.cy <= 760).sort((a2, b2) => a2.kk < b2.kk ?-1 : 1) : [];
12439    const dMin = dotsRaw.length ?Math.min.apply(null, dotsRaw.map((d2) => d2.v)) : 0;
12440    const dMax = dotsRaw.length ?Math.max.apply(null, dotsRaw.map((d2) => d2.v)) : 0;
12441    const dT = (v) => dMax > dMin ?(v - dMin) / (dMax - dMin) : 0.5;
12442    const dCol = (v) => 'hsl(211 70% ' + (78 - dT(v) * 50).toFixed(1) + '%)';
12443    const mapProvs = provs.map((p) => {
12444      const c = byProv[p.key], v = val(c);
12445      const isSel = sel === p.key;
12446      return {
12447        d: p.d, fill: view === 'loc' ?'#eef2f7' : col(v), stroke: isSel ?'#e63946' : '#fff', sw: isSel ?'2' : '0.8',
12448        click: () => this.setState({ mapSel: isSel ?null : p.key }),
12449        move: (e) => this.mapTipMove(e, p.name, v ?fmtV(v) + unit + ' · ' + (c ?c.lots : 0) + ' lotes' : 'Sin datos en el período'),
12450        leave: () => this.setState({ mapTip: null }),
12451      };
12452    });
12453    const ranked = present.slice().sort((a, b) => b.v - a.v);
12454    const totLots = present.reduce((a, x) => a + (x.c.lots || 0), 0);
12455    const selEntry = sel ?provs.find((p) => p.key === sel) : null;
12456    const selCell = sel ?byProv[sel] : null;
12457    const selVal = val(selCell);
12458    const locs = selCell ?Object.entries(selCell.locs).map(([n, L]) => ({ n, v: metric === 'kg' ?(L.nk ?L.sk / L.nk : 0) : (L.nc ?L.sc / L.nc : 0) })).filter((l) => l.v > 0).sort((a, b) => b.v - a.v).slice(0, 4) : [];
12459    const locMax = locs.length ?Math.max.apply(null, locs.map((l) => l.v)) : 1;
12460    const consList = selCell && selCell.cons ?Object.entries(selCell.cons).map(([n, C]) => ({ n, lots: C.lots, v: metric === 'kg' ?(C.nk ?C.sk / C.nk : 0) : (C.nc ?C.sc / C.nc : 0) })).filter((c3) => c3.v > 0).sort((a, b) => b.lots - a.lots).slice(0, 5) : [];
12461    const consMax = consList.length ?Math.max.apply(null, consList.map((c3) => c3.v)) : 1;
12462    const catLabel = cat === '__all__' ?'Todas las categorías' : cat;
12463    const tip = s.mapTip;
12464    return {
12465      mapReady: true, mapLoading: false, mapDemo: !!s.mapDemo, mapFetching: !!s.mapFetching,
12466      mapYearLabel: '' + year,
12467      mapLotsLabel: totLots ?totLots.toLocaleString('es-AR') + ' lotes · ' + catLabel : 'Sin datos para el período',
12468      mapMetricLabel: metric === 'kg' ?'promedio $/kg' : 'promedio $/cabeza',
12469      mapMetricUnit: metric === 'kg' ?'$/kg promedio' : '$/cabeza promedio',
12470      mapMetricTabs: [{ k: 'kg', l: '$ / kg' }, { k: 'cab', l: '$ / cabeza' }].map((m) => { const on = metric === m.k; return { label: m.l, pick: () => this.setState({ mapMetric: m.k }), bg: on ?'#002e5f' : '#fff', color: on ?'#fff' : '#3d4c5f', border: on ?'#002e5f' : '#dde7f2' }; }),
12471      mapCatTabs: [{ k: '__all__', l: 'Todas' }].concat((this._mapCats || []).map((c2) => ({ k: c2, l: c2 }))).map((o) => { const on = cat === o.k; return { label: o.l, pick: () => this.setState({ mapCat: o.k }), bg: on ?'#0a5ca8' : '#fff', color: on ?'#fff' : '#3d4c5f', border: on ?'#0a5ca8' : '#dde7f2' }; }),
12472      mapYearTabs: [...new Set([].concat(this._mapYears || [], this._mapTargetYears || []))].sort((a2, b2) => a2 - b2).map((y2) => { const loaded = (this._mapYears || []).includes(y2); const loading = !loaded && !!s.mapFetching; const on = y2 === year; return { label: '' + y2, loading, loaded, pick: () => { if (!loaded) return; this._userPickedYear = true; if (this._mapPlayT) { clearInterval(this._mapPlayT); this._mapPlayT = null; } this.setState({ mapYear: y2, mapMonth: null }); }, bg: on ?'#002e5f' : '#f2f6fb', color: on ?'#fff' : (loaded ?'#3d4c5f' : '#a9b6c6'), border: on ?'#002e5f' : '#e3ecf5' }; }),
12473      mapHasMonths: view !== 'prov' ?monthsY.length >= 1 : monthsY.length >= 2,
12474      mapMonthMax: monthsY.length,
12475      mapMonthIndex: useMonth ?monthsY.indexOf(month) + 1 : 0,
12476      mapMonthLabel: useMonth ?(this.MES_ES[month - 1].toUpperCase() + ' ' + year) : ('AÑO COMPLETO ' + year),
12477      mapMonthSet: (e) => { const v = +e.target.value; this._userPickedYear = true; if (this._mapPlayT) { clearInterval(this._mapPlayT); this._mapPlayT = null; } this.setState({ mapMonth: v === 0 ?null : monthsY[v - 1] }); },
12478      mapPlayIcon: this._mapPlayT ?'M7 5h4v14H7zM13 5h4v14h-4z' : 'M8 5l12 7-12 7z',
12479      mapPlayToggle: () => this.toggleMapPlay(),
12480      mapProvs,
12481      mapLegendMin: view === 'loc' ?(dotsRaw.length ?fmtV(dMin) : '—') : (present.length ?fmtV(vMin) : '—'),
12482      mapLegendMax: view === 'loc' ?(dotsRaw.length ?fmtV(dMax) : '—') : (present.length ?fmtV(vMax) : '—'),
12483      mapHasPts: !!(this._projOk && this._hasPts),
12484      mapViewTabs: [{ k: 'prov', l: 'Provincias' }, { k: 'loc', l: 'Localidades' }].map((m2) => { const on = view === m2.k; return { label: m2.l, pick: () => this.setState({ mapView: m2.k }), bg: on ?'#002e5f' : 'transparent', color: on ?'#fff' : '#3d4c5f' }; }),
12485      mapHeatView: view === 'loc', mapProvView: view !== 'loc',
12486      mapClip: view === 'loc' ?provs.map((p) => ({ d: p.d })) : [],
12487      mapHeat: heatUnion.map((d2) => {
12488        const on = d2.v > 0;
12489        return {
12490          key: d2.kk, cx: d2.cx.toFixed(1), cy: d2.cy.toFixed(1),
12491          r: on ?Math.min(62, 22 + Math.sqrt(d2.lots) * 3.6).toFixed(1) : '0',
12492          fill: on ?this.heatColor(dT(d2.v)) : 'rgb(38,128,196)', op: on ?'0.62' : '0',
12493        };
12494      }),
12495      mapDots: heatUnion.filter((d2) => d2.v > 0).sort((a2, b2) => b2.lots - a2.lots).map((d2) => {
12496        const provName = (provs.find((pp) => pp.key === d2.prov) || {}).name || d2.prov;
12497        return {
12498          key: d2.kk, cx: d2.cx.toFixed(1), cy: d2.cy.toFixed(1), r: Math.max(7, Math.min(14, 5 + Math.sqrt(d2.lots) * 1.1)).toFixed(1),
12499          click: () => this.setState({ mapSel: d2.prov }),
12500          move: (e) => this.mapTipMove(e, d2.name + ' · ' + provName, fmtV(d2.v) + unit + ' · ' + d2.lots + ' lotes'),
12501          leave: () => this.setState({ mapTip: null }),
12502        };
12503      }),
12504      mapTipShow: !!tip, mapTipX: tip ?tip.x + 'px' : '0px', mapTipY: tip ?tip.y + 'px' : '0px', mapTipName: tip ?tip.name : '', mapTipTxt: tip ?tip.txt : '',
12505      mapHasSel: !!(sel && selCell), mapNoSel: !(sel && selCell),
12506      mapSelKey: (sel || '') + year + metric,
12507      mapSelName: selEntry ?selEntry.name : '', mapClearSel: () => this.setState({ mapSel: null }),
12508      mapSelValRoll: this.rollEl(selVal ?fmtV(selVal) : '—'),
12509      mapSelStats: selCell ? [
12510        { label: 'LOTES', val: selCell.lots.toLocaleString('es-AR') },
12511        { label: 'CABEZAS', val: selCell.heads.toLocaleString('es-AR') },
12512        { label: '$/KG PROM', val: selCell.nk ?this.fmtMoney(selCell.sk / selCell.nk, 0) : '—' },
12513        { label: '$/CAB PROM', val: selCell.nc ?this.fmtCompact(selCell.sc / selCell.nc) : '—' },
12514      ] : [],
12515      mapSelHasLocs: locs.length > 0,
12516      mapSelLocs: locs.map((l) => ({ name: l.n, val: fmtV(l.v), pct: Math.round((l.v / locMax) * 100) + '%' })),
12517      mapSelHasCons: consList.length > 0,
12518      mapSelCons: consList.map((c3) => ({ name: c3.n, val: fmtV(c3.v), lots: c3.lots + (c3.lots === 1 ?' lote' : ' lotes'), pct: Math.round((c3.v / consMax) * 100) + '%' })),
12519      mapCatLabel: catLabel,
12520      mapRanking: ranked.slice(0, 6).map((x, i) => ({ pos: i + 1, name: x.p.name, val: fmtV(x.v), pct: Math.round(tt(x.v) * 88 + 12) + '%', barBg: i === 0 ?'linear-gradient(90deg, #0a5ca8, #002e5f)' : '#9db9d6', pick: () => this.setState({ mapSel: x.p.key }) })),
12521    };
12522  }
12523  async unwrap(url, opts) {
12524    try {
12525      const normalizedUrl = String(url || '').replace(/\s+\|\|\s+/, '?');
12526      const r = await fetch(normalizedUrl, opts);
12527      if (!r.ok) throw new Error(r.status);
12528      const j = await r.json();
12529      return (j && typeof j === 'object' && 'data' in j) ?j.data : j;
12530    } catch { return null; }
12531  }
12532  scrollHomeChannels(direction) {
12533    const list = document.querySelector('.home-channel-programs');
12534    list?.scrollBy({left:direction*Math.max(180,list.clientWidth*.8),behavior:'smooth'});
12535  }
12536  homeChannelItems() {
12537    return [...(this.state.monthRemates || []), ...(this.state.rosganRemates || []), ...(this.state.cabanas || []), ...(this.state.videos || []).filter(v => v.scheduledStartTime || v.actualStartTime)];
12538  }
12539  selectHomeChannel(item, event) {
12540    if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return;
12541    const selected = window.RosganHomeChannel.normalizeSpot(item) || window.RosganHomeChannel.normalize(item);
12542    event?.preventDefault();
12543    if (!selected) { this.showToast('Esta transmisión todavía no tiene video asignado.'); return; }
12544    this.setState({ homeChannelManual: selected }, () => document.getElementById('home-channel-player')?.scrollIntoView({ behavior: 'smooth', block: 'center' }));
12545  }
12546  homeChannelView() {
12547    const api = window.RosganHomeChannel;
12548    const now = this.state.homeChannelNow || Date.now();
12549    const items = this.homeChannelItems();
12550    const spots = (this.state.homeSpots || []).filter(spot => !(this.state.homeSpotFailures || []).includes(spot.id));
12551    const selected = api.select({ items, cafes: this.state.cafeVideos || [], spots, spotIndex: this.state.homeSpotIndex || 0, manual: this.state.homeChannelManual, now });
12552    const seen = new Set();
12553    const today = [...items, ...(this.state.cafeVideos || [])].map(item => ({item, video:api.normalize(item)})).filter(({video}) => {
12554      if (!video || video.day !== api.clock(now).day || seen.has(video.id)) return false;
12555      seen.add(video.id); return true;
12556    });
12557    return {
12558      hcIsSpot: !!selected?.spot, hcHasYoutube: !!selected && !selected.spot, hcSpotUrl: selected?.videoUrl || '',
12559      hcSpotLoop: spots.length === 1 ? 'true' : 'false',
12560      hcSpotEnded: () => { const currentIndex = spots.findIndex(spot => api.normalizeSpot(spot)?.id === selected?.id); const nextIndex = (currentIndex + 1) % Math.max(1,spots.length); this.setState({homeSpotIndex:nextIndex, ...(this.state.homeChannelManual?.spot ? {homeChannelManual:api.normalizeSpot(spots[nextIndex])} : {})}); },
12561      hcSpotError: () => { if (selected?.spot) this.setState({homeSpotFailures:[...(this.state.homeSpotFailures || []), selected.id.slice(5)]}); },
12562      hcHasVideo: !!selected, hcEmpty: !selected, hcPlayerKey:selected?.spot ? selected.id + ':' + (this.state.homeSpotIndex || 0) : selected?.id || '', hcEmbed:selected?.embed || '', hcTitle:selected?.title || 'La programación de hoy', hcHref:selected?.href || '',
12563      hcManual:!!this.state.homeChannelManual, hcAutomatic:() => this.setState({homeChannelManual:null, homeChannelNow:Date.now()}),
12564      hcLiveClass:selected?.live ? 'is-live' : '', hcBadge:selected?.spot ? 'INSTITUCIONAL' : selected?.live ? '● V
12564IVO' : selected?.ended ? 'GRABADO' : selected ? 'PROGRAMADO' : 'ROSGAN',
12565      hcReason:selected?.reason || 'No hay transmisiones programadas.', hcEmptyTitle:this.state.monthRemates===null ? 'Cargando programación…' : 'Sin programación disponible',
12566      hcHasSpots:spots.length>0, hcSpots:spots.map(spot=>({title:spot.title || 'Spot institucional',selected:api.normalizeSpot(spot)?.id===selected?.id,select:()=>this.selectHomeChannel(spot)})),
12567      hcHasToday:today.length>0, hcToday:today.map(({item,video})=>({title:video.title,label:video.cafe ? (video.start && api.clock(video.start).minute >= 720 ? 'Café de la Tarde' : 'Café Ganadero') : (item.nombre_cuenta || item.consignatariaNombre || video.title),thumb:video.thumb,selected:video.id===selected?.id,time:video.hasTime ? new Intl.DateTimeFormat('es-AR',{timeZone:'America/Argentina/Buenos_Aires',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).format(new Date(video.start))+' h' : 'Horario a confirmar',select:()=>this.selectHomeChannel(item)})), hcPreviousChannels:()=>
12567this.scrollHomeChannels(-1), hcNextChannels:()=>this.scrollHomeChannels(1),
12568    };
12569  }
12570  async refreshHomeChannel() {
12571    if (this.state.route !== 'home') return;
12572    this.setState({ homeChannelNow: Date.now() });
12573    if (this._homeDataRequest || this._homeChannelRefreshing || Date.now()-(this._homeChannelFetchedAt || 0)<300000) return;
12574    this._homeChannelRefreshing = true;
12575    this._homeChannelFetchedAt = Date.now();
12576    try {
12577      const [channel,cafe,spots] = await Promise.all([this.unwrap('/api/youtube/channel'),this.unwrap('/api/youtube/cafe-ganadero'),window.RosganHomeChannel.fetchSpots().catch(() => null)]);
12578      const patch = {};
12579      if (spots) patch.homeSpots = spots;
12580      if (channel) patch.videos = [...(channel.live || []),...(channel.upcoming || []),...(channel.recent || [])];
12581      if (cafe) patch.cafeVideos = cafe.cafeGanadero?.videos || cafe.videos || [];
12582      this.setState(patch);
12583      await this.fetchMonthRemates();
12584    } finally { this._homeChannelRefreshing = false; }
12585  }
12586  async fetchData() {
12587    if (this._homeDataRequest) return this._homeDataRequest;
12588    // Publish each section as it arrives; public content must not wait for
12589    // authentication, the private Feria list, or a slow YouTube response.
12590    const sessionEpoch = this._sessionEpoch || 0;
12591    const publish = patch => {
12592      if (sessionEpoch === (this._sessionEpoch || 0)) this.setState(patch);
12593    };
12594    let channelCafe = null;
12595    const jobs = [
12596      this.fetchMonthRemates(), this.loadRosganRemateAdmin(),
12597      this.fetchNews(), this.fetchIndices(),
12598      window.RosganHomeChannel.fetchSpots().then(spots => publish({homeSpots:spots})).catch(() => {}),
12599      this.unwrap('/api/consignatarias').then(cons => {
12600        const items = Array.isArray(cons) ? cons : (cons?.items || []);
12601        publish({ consignatarias: Array.isArray(items) ? items : [] });
12602      }),
12603      this.unwrap('/api/youtube/channel').then(channel => {
12604        channelCafe = channel?.cafeGanadero;
12605        publish({ videos: [...(channel?.live || []), ...(channel?.upcoming || []), ...(channel?.recent || [])],
12606          ...(channelCafe ? { cafe: channelCafe, cafeVideos: channelCafe.videos || [] } : {}) });
12607      }),
12608      this.unwrap('/api/youtube/cafe-ganadero').then(cafe => {
12609        const value = channelCafe || cafe?.cafeGanadero || cafe || {};
12610        publish({ cafe: value, cafeVideos: value.videos || [] });
12611      }),
12612      (async () => {
12613        const token = await this.authToken();
12614        const ferias = token ? await this.unwrap('/api/ferias', { headers: { Accept: 'application/json', Authorization: 'Bearer ' + token } }) : null;
12615        const items = Array.isArray(ferias) ? ferias : (ferias?.items || ferias?.ferias || []);
12616        publish({ ferias: Array.isArray(items) ? items : [] });
12617      })(),
12618    ];
12619    this._homeChannelFetchedAt = Date.now();
12620    this._homeDataRequest = Promise.allSettled(jobs);
12621    try { await this._homeDataRequest; }
12622    finally { this._homeDataRequest = null; }
12623  }
12624  async fetchNews() {
12625    const nb = 'https://us-central1-studio-4500177996-48a75.cloudfunctions.net';
12626    const [lote, noti] = await Promise.all([
12627      this.unwrap(nb + '/getLoteNoticias'),
12628      this.unwrap(nb + '/getNoticiasConHero'),
12629    ]);
12630    this.setState({
12631      loteNoticias: Array.isArray(lote) ?lote : (Array.isArray(lote?.items) ?lote.items : []),
12632      noticias: Array.isArray(noti) ?noti : (Array.isArray(noti?.items) ?noti.items : []),
12633    });
12634  }
12635  async fetchIndices() {
12636    const response = await this.apiFetch('/api/market-indices');
12637    const idx = response?.data;
12638    if (!idx || !Array.isArray(idx) || !idx.length) {
12639      this.setState({ indices: [] });
12640      return;
12641    }
12642    const sorted = idx.slice().sort((a, b) => new Date(b.fecha_remate) - new Date(a.fecha_remate));
12643    this.setState({ indices: sorted });
12644  }
12645  MES_ES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
12646  DigitRollComp() {
12647    if (this._Roll) return this._Roll;
12648    const R = React;
12649    this._Roll = class extends R.Component {
12650      render() {
12651        const p = this.props;
12652        const chars = String(p.text == null ?'' : p.text).split('');
12653        return R.createElement('span', { style: { display: 'inline-flex', alignItems: 'flex-end', lineHeight: 1 } },
12654          chars.map((ch, i) => {
12655            if (/[0-9]/.test(ch)) {
12656              const d = parseInt(ch, 10);
12657              const reel = [];
12658              for (let k = 0; k <= 9; k++) reel.push(R.createElement('span', { key: k, style: { display: 'block', height: '1em', lineHeight: '1em' } }, String(k)));
12659              return R.createElement('span', { key: i, style: { display: 'inline-block', height: '1em', lineHeight: '1em', overflow: 'hidden', verticalAlign: 'bottom' } },
12660                R.createElement('span', { style: { display: 'block', transform: 'translateY(-' + d + 'em)', transition: 'transform 850ms cubic-bezier(0.16,1.02,0.3,1)', transitionDelay: (i * 55) + 'ms' } }, reel));
12661            }
12662            return R.createElement('span', { key: i, style: { display: 'inline-block' } }, ch);
12663          }));
12664      }
12665    };
12666    return this._Roll;
12667  }
12668  rollEl(text) { return React.createElement(this.DigitRollComp(), { text }); }
12669  fmtMoney(n, dec) { const v = Number(n) || 0;
12669 return '$' + v.toLocaleString('es-AR', { minimumFractionDigits: dec || 0, maximumFractionDigits: dec || 0 }); }
12670  fmtCompact(n) { const v = Number(n) || 0; if (v >= 1e6) return '$' + (v / 1e6).toFixed(2) + 'M'; if (v >= 1e3) return '$' + (v / 1e3).toFixed(0) + 'k'; return '$' + Math.round(v); }
12671  indexSeries(key) { return (this.state.indices || []).slice().reverse().map((d) => ({ v: Number(d[key]) || 0, label: this.MES_ES[(d.mes_remate || 1) - 1] + ' ' + String(d.anio_remate).slice(2), fecha: d.fecha_remate })).filter((p) => p.v > 0); }
12672  prevNonZero(key, fromIdx) { const arr = this.state.indices || []; for (let i = fromIdx + 1; i < arr.length; i++) { if (Number(arr[i][key]) > 0) return Number(arr[i][key]); } return null; }
12673  indexCards() {
12674    const arr = this.state.indices || [];
12675    if (!arr.length) return [];
12676    const defs = [
12677      { key: 'piri', label: 'PIRI', sub: 'Precio Índice Rosgan Invernada', dec: 2, unit: '/kg' },
12678      { key: 'pirc', label: 'PIRC', sub: 'Precio Índice Rosgan Cría', dec: 0, unit: '/vientre', compact: true },
12679      { key: 'indice_ternero', label: 'Índice Ternero', sub: 'Promedio ternero invernada', dec: 2, unit: '/kg' },
12680    ];
12681    return defs.map((d) => {
12682      let ci = 0; while (ci < arr.length && !(Number(arr[ci][d.key]) > 0)) ci++;
12683      const cur = Number(arr[ci]?.[d.key]) || 0;
12684      const prev = this.prevNonZero(d.key, ci);
12685      const pct = prev ?((cur - prev) / prev) * 100 : 0;
12686      const dir = pct > 0.05 ?'up' : pct < -0.05 ?'down' : 'flat';
12687      return {
12688        ...d, cur, prev, pct, dir,
12689        curFmt: d.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, d.dec),
12690        rollEl: this.rollEl(d.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, d.dec)),
12691        pctFmt: (pct > 0 ?'+' : '') + pct.toFixed(1) + '%',
12692        color: dir === 'up' ?'#1fbf75' : dir === 'down' ?'#ff5a6a' : '#c8d8ea',
12693        arrow: dir === 'up' ?'M6 14l6-6 6 6' : dir === 'down' ?'M6 10l6 6 6-6' : 'M6 12h12',
12694        mes: this.MES_ES[(arr[ci]?.mes_remate || 1) - 1] + ' ' + arr[ci]?.anio_remate,
12695      };
12696    });
12697  }
12698  async fetchAgendaMonth(month) {
12699    const [historical, ferias] = await Promise.all([
12700      this.unwrap('/api/remateshistorico?scope=month&year=' + month.slice(0,4) + '&month=' + month.slice(5)),
12701      this.unwrap('/api/ferias/agenda?month=' + encodeURIComponent(month)),
12702    ]);
12703    const seen = new Set();
12704    const items = [...(ferias?.items || []), ...(historical?.items || [])].filter(item => {
12705      const video = item.youtubeVideoId || item.youtubeApiId;
12706      const key = video ?'youtube:' + video : this.consKey(item.nombre_remate || '') + '|' + String(item.fecha_remate || '').slice(0,10) + '|' + this.consKey(item.nombre_cuenta || '');
12707      if (seen.has(key)) return false;
12708      seen.add(key); return true;
12709    });
12710    return {items};
12711  }
12712  async fetchMonthRemates() {
12713    const start = new Date(this.homeAgendaDay(new Date()));
12714    const end = new Date(start.getTime() + 30 * 86400000);
12715    const months = new Set();
12716    for (let date = new Date(start); date <= end; date.setUTCDate(date.getUTCDate() + 1)) months.add(date.toISOString().slice(0, 7));
12717    const results = await Promise.allSettled([...months].map((month) => this.fetchAgendaMonth(month)));
12718    const seen = new Set();
12719    const items = results.flatMap((result) => result.status === 'fulfilled' && Array.isArray(result.value?.items) ?result.value.items : []).filter((item) => {
12720      const key = String(item.id || item.id_remate || [item.nombre_remate, item.fecha_remate, item.nombre_cuenta].join('|'));
12721      if (seen.has(key)) return false;
12722      seen.add(key); return true;
12723    });
12724    this.setState({ monthRemates: items });
12725    this.loadRemateListCatalogs(items);
12726    if (items.length) this.startHeroAuto();
12727  }
12728  startHeroAuto() {
12729    if (!(this.props.animaciones || true)) return;
12730    clearInterval(this._heroTimer);
12731    this._heroTimer = setInterval(() => {
12732      const n = this._heroCount || 1;
12733      if (this.state.route === 'home' && n > 0 && !this._heroGesture && Date.now() > (this._heroPauseUntil || 0)) this.selectHomeHero((this.state.heroSlide + 1) % n);
12734      const hn = (this.homeHeroImages || []).length;
12735      if (this.state.route === 'home' && hn > 1) this.setState((s) => ({ homeHeroSlide: (s.homeHeroSlide + 1) % hn }));
12736      const nn = this._notiCount || 0;
12737      if (this.state.route === 'home' && nn > 1) this.setState((s) => ({ notiSlide: (s.notiSlide + 1) % nn }));
12738    }, 10000);
12739  }
12740  moveHomeHero(step) {
12741    const count = this._heroCount || 1;
12742    this._heroPauseUntil = Date.now() + 10000;
12743    this.selectHomeHero(((this.state.heroSlide + step) % count + count) % count);
12744  }
12745  rosganBanner(ordinal = 0) {
12746    return Math.abs(ordinal) % 2 ?{ src:'assets/home/rosgan-remate-bolsa.webp', position:'center 45%' } : { src:'assets/home/rosgan-remate-microfono.webp', position:'center 58%' };
12747  }
12748  rosganAnniversaryImage(item) {
12749    return String(item?.id || item?.id_remate || '') === '5'
12750      && this.consKey(item?.nombre_remate || item?.titulo) === 'granremateaniversario'
12751      ?'assets/home/gran-remate-aniversario-ecu-2026.webp' : '';
12752  }
12753  selectHomeHero(index) {
12754    const rosgan = this._homeHeroRosganIndices || [];
12755    this.setState(s => {
12756      let visit = Number.isInteger(s.homeRosganBannerIndex) ?s.homeRosganBannerIndex : (rosgan.includes(s.heroSlide) ?0:-1);
12757      if (rosgan.includes(index) && (index !== s.heroSlide || this._heroCount === 1)) visit++;
12758      return { heroSlide:index, homeRosganBannerIndex:visit };
12759    });
12760  }
12761  startHomeHeroGesture(event) {
12762    if (event.isPrimary === false || (event.pointerType === 'mouse' && event.button !== 0) || (this._heroCount || 0) < 2) return;
12763    if (event.target.closest('a,button,input,select,.home-hero-actions,.home-hero-indices,.home-hero-dots')) return;
12764    this._heroGesture = { id: event.pointerId, x: event.clientX, y: event.clientY };
12765    event.currentTarget.setPointerCapture(event.pointerId);
12766  }
12767  endHomeHeroGesture(event, cancelled = false) {
12768    const start = this._heroGesture;
12769    this._heroGesture = null;
12770    if (!start || start.id !== event.pointerId) return;
12771    if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
12772    const dx = event.clientX - start.x, dy = event.clientY - start.y;
12773    if (!cancelled && Math.abs(dx) >= 45 && Math.abs(dx) > Math.abs(dy) * 1.4) this.moveHomeHero(dx < 0 ?1 : -1);
12774  }
12775  histHeroThumb(it) {
12776    const anniversaryImage = this.rosganAnniversaryImage(it);
12777    if (anniversaryImage) return anniversaryImage;
12778    const branded = it.bannerUrl || it.backgroundUrl || this.consBackground(this.remateConsignatariaRef(it));
12779    if (branded) return branded;
12780    const d = it.youtube?.thumbnailUrl || it.thumbnailUrl;
12781    if (d) return d;
12782    const id = it.youtubeApiId || it.youtubeVideoId || it.youtube?.id;
12783    if (id) return 'https://i.ytimg.com/vi/' + id + '/maxresdefault.jpg';
12784    return '';
12785  }
12786  histThumb(it) { return this.histHeroThumb(it) || 'assets/feria.png'; }
12787  async loadHomeCalendar(month) {
12788    const today = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date());
12789    month = month || this.state.homeCalendarMonth || today.slice(0, 7);
12790    this.setState({ homeCalendarMonth: month, homeCalendarDay: '', homeCalendarPage: 0 });
12791    this._calendarRequest = month;
12792    if (this.state.homeCalendarCache?.[month]) { this.setState({ homeCalendarLoading: false, homeCalendarError: '' }); return; }
12793    this.setState({ homeCalendarLoading: true, homeCalendarError: '' });
12794    try {
12795      const data = await this.fetchAgendaMonth(month);
12796      if (!Array.isArray(data?.items)) throw new Error('Invalid agenda');
12797      this.setState(s => ({ homeCalendarCache: { ...s.homeCalendarCache, [month]: data.items } }));
12798      if (this._calendarRequest === month) this.setState({ homeCalendarLoading: false });
12799    } catch {
12800      if (this._calendarRequest === month) this.setState({ homeCalendarLoading: false, homeCalendarError: 'No pudimos completar la agenda. Volvé a intentar.' });
12801    }
12802  }
12803  homeCalendarView() {
12804    const s = this.state, tz = 'America/Argentina/Buenos_Aires';
12805    const dayOf = date => date && !isNaN(date) ?new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date) : '';
12806    const today = dayOf(new Date()), month = s.homeCalendarMonth || today.slice(0, 7);
12807    const [year, mon] = month.split('-').map(Number);
12808    const monthDate = new Date(Date.UTC(year, mon - 1, 1, 12));
12809    const selected = s.homeCalendarDay || '';
12810    const candidates = [
12811      ...(s.cabanas || []).filter(it => !this.isExcludedCabana(it)).map(it => ({ it, kind: 'Cabañas', date: this.cabanaAuctionStartAt(it), open: () => this.openCabana(it) })),
12812      ...(s.rosganRemates || []).filter(it => !this.isExcludedRemate(it)).map(it => ({ it, kind: 'Rosgan', date: this.remateDate(it), open: () => this.openRemate(it) })),
12813      ...(s.homeCalendarCache?.[month] || s.monthRemates || []).filter(it => !this.isExcludedRemate(it)).map(it => ({ it, kind: this.remateType(it)[0], date: this.remateDate(it), open: () => this.openRemate(it) })),
12814    ];
12815    const seen = new Set();
12816    const events = candidates.map(({ it, kind, date, open }) => {
12817      const raw = it.fecha_remate || it.fecha || it.date || it.schedule?.fecha || '';
12818      const day = /^\d{4}-\d{2}-\d{2}$/.test(raw) ?raw : dayOf(date);
12819      const title = it.nombre_remate || it.titulo || it.title || it.nombre || 'Remate';
12820      const cons = this.remateConsignatariaRef(it) || 'Rosgan';
12821      const isRosgan = this.consKey(cons) === 'rosgan';
12822      const whiteLogo = isRosgan ?'' : this.consWhiteLogo(cons);
12823      const consLogo = isRosgan ?'assets/rosgan-logo-blue.png' : whiteLogo || this.consLogo(cons);
12824      const place = it.ubicacion_remate || it.lugar || it.location || it.ubicacion;
12825      const clock = it.hora_remate || it.hora || it.schedule?.hora;
12826      return { day, title, kind: /cabaña|cabana/i.test(title)?'Cabañas':kind, open, key: this.consKey(title) + '|' + day,
12827        time: clock ?String(clock).slice(0, 5) + ' h' : date && !/^\d{4}-\d{2}-\d{2}$/.test(raw) ?date.toLocaleTimeString('es-AR', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12:false }) + ' h' : 'Horario a confirmar',
12828        location: typeof place === 'string' ?place : [place?.lugar || place?.nombre || place?.name || place?.address, place?.localidad || place?.locality, place?.provinciaNombre || place?.provincia || place?.province].filter(v=>typeof v==='string'&&v).join(', ') || 'Ubicación a confirmar',
12829        cons: this.consRecord(cons)?.name || this.consRecord(cons)?.nombre || cons,
12830        consLogo, hasConsLogo:!!consLogo, noConsLogo:!consLogo, consLogoBg:whiteLogo?this.consBrandColor(cons):isRosgan||consLogo?'#fff':'#002e5f', consInitial:String(cons).trim().slice(0,1).toUpperCase(),
12831        number: day.slice(8), dateLabel: day ?new Date(day + 'T12:00:00Z').toLocaleDateString('es-AR', { timeZone: tz, weekday: 'short', day: 'numeric', month: 'short' }) : '',
12832      };
12833    }).filter(e => { if (!e.day.startsWith(month) || seen.has(e.key)) return false; seen.add(e.key); return true; }).sort((a,b) => a.day.localeCompare(b.day) || a.time.localeCompare(b.time));
12834    const days = [], offset = (monthDate.getUTCDay() + 6) % 7;
12835    const total = new Date(Date.UTC(year, mon, 0)).getUTCDate();
12836    for (let i=0; i<Math.ceil((offset+total)/7)*7; i++) {
12837      const number=i-offset+1, valid=number>0&&number<=total;
12838      const key=valid?month+'-'+String(number).padStart(2,'0'):'';
12839      const count=valid?events.filter(e=>e.day===key).length:0;
12840      days.push({ number: valid?number:'', disabled:!valid, label:valid?`${number} de ${monthDate.toLocaleDateString('es-AR',{month:'long',timeZone:tz})}: ${count} remates`:'Fuera del mes',
12841        css: 'agenda-day'+(!valid?' is-blank':'')+(key===today?' is-today':'')+(key===selected?' is-selected':'')+(count?' has-events':''),
12842        count, hasEvents:count>0, pressed:valid&&selected===key?'true':'false', pick:()=>valid&&this.setState({homeCalendarDay:selected===key?'':key,homeCalendarPage:0}) });
12843    }
12844    const move = delta => this.loadHomeCalendar(new Date(Date.UTC(year,mon-1+delta,1,12)).toISOString().slice(0,7));
12845    const visible = events.filter(e=>!selected||e.day===selected);
12846    const pageSize = s.isMobile ?1:3;
12847    const pages = Math.ceil(visible.length / pageSize);
12848    const page = Math.min(Math.max(0, Number(s.homeCalendarPage) || 0), Math.max(0, pages - 1));
12849    return { calendarMonthLabel: monthDate.toLocaleDateString('es-AR',{month:'long',year:'numeric',timeZone:tz}), calendarDays:days,
12850      calendarWeekdays:['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'].map(label=>({label})), calendarEvents:visible.slice(page*pageSize,page*pageSize+pageSize),
12851      calendarEventDays:days.filter(d=>d.hasEvents), calendarMonthClass:'agenda-month'+(s.homeCalendarExpanded?' is-expanded':''),
12852      calendarExpanded:s.homeCalendarExpanded?'true':'false',calendarToggleLabel:s.homeCalendarExpanded?'Ocultar calendario':'Ver calendario',calendarToggle:()=>this.setState({homeCalendarExpanded:!s.homeCalendarExpanded}),
12853      calendarHasPages:pages>1, calendarPageLabel:s.isMobile?`${page+1} de ${visible.length} remates`:`${page*3+1}–${Math.min(page*3+3,visible.length)} de ${visible.length} remates`,
12854      calendarPageFirst:page===0, calendarPageLast:page>=pages-1,
12855      calendarPagePrev:()=>this.setState({homeCalendarPage:Math.max(0,page-1)}),
12856      calendarPageNext:()=>this.setState({homeCalendarPage:Math.max(0,pages-1,0)>page?page+1:page}),
12857      calendarCount:events.length+' remates este mes', calendarListTitle:selected?new Date(selected+'T12:00:00Z').toLocaleDateString('es-AR',{weekday:'long',day:'numeric',month:'long',timeZone:tz}):'Todos los remates del mes',
12858      calendarEmpty:!visible.length&&!s.homeCalendarLoading, calendarLoading:!!s.homeCalendarLoading, calendarError:s.homeCalendarError||'', calendarHasError:!!s.homeCalendarError,
12859      calendarPrev:()=>move(-1), calendarNext:()=>move(1), calendarToday:()=>this.loadHomeCalendar(today.slice(0,7)), calendarAll:()=>this.setState({homeCalendarDay:'',homeCalendarPage:0}), calendarRetry:()=>this.loadHomeCalendar(month) };
12860  }
12861  youtubeOriginalThumb(it) {
12862    const direct = it?.youtube?.thumbnailUrl || it?.youtube?.thumbnail || it?.thumbnailUrl || it?.thumbnail;
12863    if (direct) return direct;
12864    const id = it?.youtubeApiId || it?.youtubeVideoId || it?.youtube?.id || this.ytId(it?.url_streaming || it?.youtubeUrl || '');
12865    return id ?'https://i.ytimg.com/vi/' + id + '/maxresdefault.jpg' : 'assets/feria.png';
12866  }
12867  histUrl(it) { const id = it.youtubeApiId || it.youtubeVideoId || it.youtubeStreamConfig?.broadcastId; return it.url_streaming || it.urlStream || it.youtubeStreamConfig?.watchUrl || (id ?'https://www.youtube.com/watch?v=' + id : 'https://www.youtube.com/@rosgantv'); }
12868  histMode(it) { const d = this.remateDate(it); if (!d) return 'recent'; const now = new Date(); if (d <= now && now <= new Date(d.getTime() + 4 * 3600e3)) return 'live'; return d > now ?'upcoming' : 'recent'; }
12869  homeAgendaDay(value) {
12870    const date = new Date(value);
12871    if (!value || Number.isNaN(date.getTime())) return '';
12872    return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
12873  }
12874  homeAgendaIncludes(date, now = new Date()) {
12875    const day = this.homeAgendaDay(date);
12876    const start = this.homeAgendaDay(now);
12877    const end = new Date(new Date(start).getTime() + 30 * 86400000).toISOString().slice(0, 10);
12878    return !!day && day >= start && day <= end;
12879  }
12880  async fetchLots() {
12881    this.setState({ lotsLoading: true });
12882    try {
12883      const data = await this.apiFetch('/api/Lots?limit=100');
12884      const raw = this.listFrom(data, ['data', 'items', 'lots']);
12885      const lots = raw.map((l, i) => this.mapLot(l, i));
12886      const selectedExisting = (this.state.lots || []).find((lot) => lot.cert === this.state.selectedCert);
12887      if (selectedExisting && !lots.some((lot) => lot.cert === selectedExisting.cert)) lots.push(selectedExisting);
12888      this.setState({ lots, lotsReal: true, lotsLoading: false }, () => {
12889        if (this.state.route === 'lot' && this.state.selectedCert) {
12890          this.fetchLotBids(this.state.selectedCert);
12891          this.fetchLotSellerConsignataria(this.currentLot());
12892        }
12893      });
12894    } catch {
12895      const selectedExisting = (this.state.lots || []).find((lot) => lot.cert === this.state.selectedCert);
12896      this.setState({ lots: selectedExisting ?[selectedExisting] : [], lotsReal: !!selectedExisting, lotsLoading: false });
12897    }
12898  }
12899  async fetchRoslotLotByCert(cert) {
12900    const targetCert = String(cert || '').trim();
12901    if (!targetCert || this._roslotLotFetchCert === targetCert) return;
12902    this._roslotLotFetchCert = targetCert;
12903    try {
12904      const data = await this.apiFetch('/api/Lots/' + encodeURIComponent(targetCert));
12905      const root = data?.data ?? data?.lot ?? data?.item ?? data;
12906      const raw = root?.lot ?? root?.item ?? root;
12907      if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
12908      const mapped = this.mapLot(raw, 0);
12909      if (mapped.cert !== targetCert) return;
12910      this.setState((previous) => ({
12911        lots: [mapped, ...(previous.lots || []).filter((lot) => lot.cert !== targetCert)],
12912        lotsReal: true,
12913      }), () => {
12914        if (this.state.route === 'lot' && this.state.selectedCert === targetCert) {
12915          this.fetchLotBids(targetCert);
12916          this.fetchLotSellerConsignataria(mapped);
12917        }
12918      });
12919    } catch (error) {
12920      if (![404, 410].includes(Number(error?.status))) console.warn('[Roslot lot deep link failed]', targetCert, error?.message || error);
12921    } finally {
12922      if (this._roslotLotFetchCert === targetCert) this._roslotLotFetchCert = '';
12923    }
12924  }
12925  async syncAuctionStatuses() {
12926    if (this._auctionStatusSyncing || document.hidden) return;
12927    this._auctionStatusSyncing = true;
12928    try {
12929      const data = await this.apiFetch('/api/Lots?limit=100');
12930      const raw = this.listFrom(data, ['data', 'items', 'lots']);
12931      const mappedLots = raw.map((lot, index) => this.mapLot(lot, index));
12932      const patch = {};
12933      const lotsSignature = JSON.stringify(mappedLots);
12934      if (lotsSignature !== this._auctionLotsSignature) {
12935        this._auctionLotsSignature = lotsSignature;
12936        patch.lots = mappedLots;
12937        patch.lotsReal = true;
12938      }
12939      if (this.state.user && this.canManageSellerData(this.state.apiUser)) {
12940        try {
12941          const mineData = await this.apiFetch('/api/Lots/mine?limit=100');
12942          const mineRaw = this.listFrom(mineData, ['data', 'items', 'lots']);
12943          const mappedMine = mineRaw.map((lot, index) => this.mapLot(lot, index));
12944          const mineSignature = JSON.stringify(mappedMine);
12945          if (mineSignature !== this._auctionMineSignature) {
12946            this._auctionMineSignature = mineSignature;
12947            patch.myLots = mappedMine;
12948          }
12949        } catch {}
12950      }
12951      if (Object.keys(patch).length) this.setState(patch);
12952    } catch {}
12953    finally { this._auctionStatusSyncing = false; }
12954  }
12955  async syncLiveBids() {
12956    const canBuy = this.canUseBuyerData(this.state.apiUser);
12957    const canSell = this.canManageSellerData(this.state.apiUser);
12958    if (this._liveBidSyncing || document.hidden || !this.state.user || (!canBuy && !canSell)) return;
12959    this._liveBidSyncing = true;
12960    try {
12961      let myBids = this.state.myBids || [];
12962      if (canBuy) {
12963          const bidsData = await this.apiFetch('/api/Bids/mine?limit=50');
12964        myBids = this.ownBidsOnly(this.listFrom(bidsData, ['items', 'bids', 'data']).map((bid) => this.normBid(bid)).filter(Boolean));
12965      }
12966      const currentLots = this.state.lots || [];
12967      const openCerts = new Set(currentLots.filter((lot) => lot.auctionStatus === 'open').map((lot) => lot.cert));
12968      const buyerCerts = myBids
12969        .filter((bid) => openCerts.has(bid.cert) && !/(reject|rechaz|declin|lost)/i.test(bid.status))
12970        .map((bid) => String(bid.cert || ''))
12971        .filter(Boolean);
12972      const sellerCerts = canSell
12973        ? (this.state.myLots || []).filter((lot) => ['open', 'sold'].includes(lot.auctionStatus)).map((lot) => String(lot.cert || '')).filter(Boolean)
12974        : [];
12975      const activeCerts = [...new Set([...buyerCerts, ...sellerCerts])].slice(0, 24);
12976      const bidResults = await Promise.all(activeCerts.map(async (cert) => {
12977        try {
12978          const lotBidsData = await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert));
12979          const bids = this.listFrom(lotBidsData, ['items', 'bids', 'data']).map((bid) => this.normBid(bid, cert)).filter(Boolean).sort((a, b) => b.amount - a.amount);
12980          return [cert, bids];
12981        } catch { return null; }
12982      }));
12983      let pendingBids = this.state.pendingBids || [];
12984      if (canSell) {
12985        try {
12986          const pendingData = await this.apiFetch('/api/Bids/pending-approvals?limit=50');
12987          pendingBids = this.listFrom(pendingData, ['items', 'bids', 'data']).map((bid) => this.normBid(bid)).filter(Boolean);
12988        } catch {}
12989      }
12990      const nextLotBids = { ...(this.state.lotBids || {}), ...Object.fromEntries(bidResults.filter(Boolean)) };
12991      const liveSignature = JSON.stringify([canBuy ?myBids : null, canSell ?pendingBids : null, nextLotBids]);
12992      if (liveSignature !== this._liveBidSignature) {
12993        this._liveBidSignature = liveSignature;
12994        this.setState({ ...(canBuy ?{ myBids } : {}), ...(canSell ?{ pendingBids } : {}), lotBids: nextLotBids });
12995      }
12996      const now = Date.now();
12997      if (!this._lastCreditSyncAt || now - this._lastCreditSyncAt > 30000) {
12998        this._lastCreditSyncAt = now;
12999        if (this.canManageBuyerAccounts(this.state.apiUser)) this.fetchBuyerAuths();
13000      }
13001    } catch {}
13002    finally { this._liveBidSyncing = false; }
13003  }
13004  listFrom(data, keys = []) {
13005    if (Array.isArray(data)) return data;
13006    for (const key of keys) {
13007      const value = data?.[key];
13008      if (Array.isArray(value)) return value;
13009      if (value && typeof value === 'object') {
13010        for (const nested of ['items', 'data', 'lots', 'bids', 'establishments', 'authorizations', 'results', 'value']) {
13011          if (Array.isArray(value[nested])) return value[nested];
13012        }
13013      }
13014    }
13015    if (Array.isArray(data?.data)) return data.data;
13016    if (Array.isArray(data?.items)) return data.items;
13017    if (data?.data && typeof data.data === 'object') return this.listFrom(data.data, keys);
13018    return [];
13019  }
13020  async fetchMyLots() {
13021    if (this.state.myLotsLoading) return;
13022    const token = await this.authToken();
13023    if (!token) {
13024      this.setState({ myLots: null, myLotsLoading: false, myLotsError: '' });
13025      return;
13026    }
13027    this.setState({ myLotsLoading: true, myLotsError: '' });
13028    try {
13029      const data = await this.apiFetch('/api/Lots/mine?limit=100');
13030      const raw = this.listFrom(data, ['data', 'items', 'lots']);
13031      let lots = raw.map((l, i) => this.mapLot(l, i));
13032      if (!lots.length && this.state.user?.email) {
13033      const allData = await this.apiFetch('/api/Lots?limit=100');
13034        const email = String(this.state.user.email).toLowerCase();
13035        lots = this.listFrom(allData, ['data', 'items', 'lots'])
13036          .map((l, i) => this.mapLot(l, i))
13037          .filter((l) => String(l.sellerEmail || '').toLowerCase() === email);
13038      }
13039      this.setProfileFetchInfo('lotes', lots.length, '');
13040      this.setState({ myLots: lots, myLotsLoading: false, myLotsError: '' });
13041    } catch (e) {
13042      this.setProfileFetchInfo('lotes', null, e?.message || 'Error');
13043      this.setState({ myLots: null, myLotsLoading: false, myLotsError: e?.message || 'No se pudieron cargar tus lotes.' });
13044    }
13045  }
13046  setProfileFetchInfo(key, count, error) {
13047    this.setState({ profileFetchInfo: { ...(this.state.profileFetchInfo || {}), [key]: { count, error: error || '', at: new Date().toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } } });
13048  }
13049  async refreshProfileData(force = false) {
13050    const now = Date.now();
13051    if (!force && this._lastProfileRefreshAt && now - this._lastProfileRefreshAt < 30000) return;
13052    if (this._profileRefreshPromise) return this._profileRefreshPromise;
13053    this._profileRefreshPromise = this.performProfileRefresh();
13054    try {
13055      await this._profileRefreshPromise;
13056      this._lastProfileRefreshAt = Date.now();
13057    } finally {
13058      this._profileRefreshPromise = null;
13059    }
13060  }
13061  async performProfileRefresh() {
13062    const token = await this.authToken();
13063    if (!token) return;
13064    const apiUser = await this.fetchApiSession();
13065    if (!apiUser) return;
13066    const rosganProfile = this.isRosganProfile(apiUser);
13067    const canSell = !rosganProfile && this.canManageSellerData(apiUser);
13068    const canBuy = !rosganProfile && this.canUseBuyerData(apiUser);
13069    if (this.canManageRosganLots(apiUser) && this.state.user?.uid) await this.fetchUserConsignataria(this.state.user.uid);
13070    else if (this.state.userConsignataria || this.state.userConsignatarias?.length) this.setState({ userConsignataria: null, userConsignatarias: [] });
13071    if (canSell && !this.state.rosganRemates && !this.state.rosganRematesLoading) this.loadRosganRemateAdmin();
13072    if (!canSell && this.state.route === 'load') this.setState({ route: 'profile' });
13073    const jobs = [];
13074    if (rosganProfile) jobs.push(this.fetchProfileRosganLots(), this.loadRosganRemateAdmin());
13075    if (rosganProfile || this.canManageUsers(apiUser)) jobs.push(this.fetchProfileFerias());
13076    if (canSell) jobs.push(this.fetchMyLots(), this.fetchPendingApprovals());
13077    else this.setState({ myLots: [], myLotsLoading: false, myLotsError: '', pendingBids: [], pendingBidsLoading: false, pendingBidsError: '' });
13078    if (rosganProfile || canSell || this.isCertifierOnly(apiUser)) jobs.push(this.fetchComitentes(), this.fetchEstablishments());
13079    else this.setState({ comitentes: [], comitentesLoading: false, comitentesError: '', establishments: [], establishmentsLoading: false, establishmentsError: '' });
13080    if (canBuy) jobs.push(this.fetchMyBids());
13081    else this.setState({ myBids: [], myBidsLoading: false, myBidsError: '' });
13082    this.setState({ buyerAccess: [], buyerAccessLoading: false, buyerAccessError: '' });
13083    if (!rosganProfile && this.canManageBuyerAccounts(apiUser)) {
13084      jobs.push(this.fetchBuyerAccounts(), this.fetchBuyerAuths());
13085    } else {
13086      this.setState({
13087        buyerAccounts: [], buyerAccountsLoading: false, buyerAccountsError: '',
13088        buyerAuths: [], buyerAuthsLoading: false, buyerAuthsError: '',
13089        creditSummaryCache: {}, creditSummaryLoadingIds: {}, creditSummaryErrors: {},
13090        creditSummaryOpen: false, creditSummaryLoading: false, creditSummaryError: '',
13091        creditSummary: null, creditSummaryAuthorization: null,
13092        creditHistoryOpen: false, creditHistoryAuthorization: null,
13093      });
13094    }
13095    if (this.canManageUsers(apiUser) && this.state.adminUsersOpen) jobs.push(this.fetchAdminUsers());
13096    else if (!this.canManageUsers(apiUser) && (this.state.adminUsersOpen || this.state.adminUsers)) this.setState({ adminUsersOpen: false, adminUsers: null, adminSelectedUser: null });
13097    if (this.canManageUsers(apiUser)) jobs.push(this.fetchLaunchInterestsAdmin());
13098    else if (this.state.launchInterestsAdmin) this.setState({ launchInterestsAdmin: null, launchInterestsAdminTotal: 0, launchInterestsAdminError: '' });
13099    const allowedTabs = [...(rosganProfile ?['resumen', 'comitentes', 'establecimientos'] : []), ...(canSell ?['resumen', 'aprobar'] : []), ...(canSell || this.isCertifierOnly(apiUser) ?['comitentes', 'establecimientos'] : []), ...(canBuy ?['ofertas'] : [])];
13100    if (allowedTabs.length && !allowedTabs.includes(this.state.profileTab)) this.setState({ profileTab: allowedTabs[0] });
13101    await Promise.allSettled(jobs);
13102    if (canBuy || canSell) await this.syncLiveBids();
13103  }
13104  normalizeApiSessionUser(data) {
13105    if (!data || typeof data !== 'object') return null;
13106    const sources = [
13107      data,
13108      data.data,
13109      data.session,
13110      data.data?.session,
13111      data.apiSession,
13112      data.data?.apiSession,
13113      data.session?.apiSession,
13114      data.data?.session?.apiSession,
13115      data.user,
13116      data.data?.user,
13117      data.session?.user,
13118      data.data?.session?.user,
13119      data.apiSession?.user,
13120      data.session?.apiSession?.user,
13121    ].filter((value) => value && typeof value === 'object' && !Array.isArray(value));
13122    if (!sources.length) return null;
13123    const merged = Object.assign({}, ...sources);
13124    const roles = [...new Set(sources.flatMap((source) => this.userRoleIds(source)))];
13125    if (!roles.length) return merged;
13126    return {
13127      ...merged,
13128      roles,
13129      roleId: roles,
13130      role: ['sysadmin', 'admin', 'dev', 'seller', 'buyer'].find((role) => roles.includes(role)) || merged.role || 'user',
13131    };
13132  }
13133  async fetchApiSession() {
13134    const uid = this.state.user?.uid;
13135    const sessionEpoch = this._sessionEpoch || 0;
13136    if (!uid) return null;
13137    const current = () => this.state.user?.uid === uid && sessionEpoch === (this._sessionEpoch || 0);
13138    try {
13139      // The external session omits local permissions such as devAccess.
13140      // sync-user returns both the external roles and the current stored grants.
13141      const data = await this.apiFetch('/api/auth/sync-user', { method: 'POST', body: '{}' });
13142      let apiUser = this.normalizeApiSessionUser(data);
13143      if (!this.apiRoleIds(apiUser).length && ['localhost', '127.0.0.1'].includes(window.location.hostname)) {
13144        try {
13145          const localData = await this.apiFetch('/api/me');
13146          apiUser = this.normalizeApiSessionUser(localData) || apiUser;
13147        } catch {}
13148      }
13149      if (!current()) return null;
13150      if (apiUser && this.state.apiUser) {
13151        const retainedCuit = this.apiUserCuit(this.state.apiUser);
13152        apiUser = {
13153          ...this.state.apiUser,
13154          ...apiUser,
13155          ...(retainedCuit && !this.apiUserCuit(apiUser) ?{ cuitCuil: this.cuitInputDisplay(retainedCuit) } : {}),
13156        };
13157      }
13158      this.setState({ apiUser, apiSessionError: '', profilePhotoFailed: false }, () => {
13159        if (this.state.route === 'rosganRemateCreate' && !this.canManageUsers(apiUser)) {
13160          this.syncAppLocation('remates', { replace: true });
13161          this.go('remates', { syncPath: false });
13162        } else if (['remates', 'rosganRemateCreate'].includes(this.state.route) && this.canManageSellerData(apiUser)) {
13163          this.scheduleRouteWork(this.state.route);
13164        }
13165      });
13166      this.cacheApiUser(this.state.user, apiUser);
13167      return apiUser;
13168    } catch (e) {
13169      if (!current()) return null;
13170      const retained = this.canAccessApp(this.state.apiUser) ?this.state.apiUser : null;
13171      this.setState({ apiUser: retained, apiSessionError: e?.message || 'No se pudo validar la sesion con Rosgan API.' });
13172      return retained;
13173    }
13174  }
13175  profileApiData() {
13176    const user = this.state.user ? (this.state.apiUser || {}) : {};
13177    return user.profile && typeof user.profile === 'object' ?{ ...user, ...user.profile } : user;
13178  }
13179  profilePhotoUrl(user = this.profileApiData()) {
13180    const candidates = [user?.profilePhotoUrl, user?.photoUrl, user?.photoURL, user?.avatarUrl, user?.profilePhoto, user?.photo];
13181    const value = candidates.find((candidate) => typeof candidate === 'string' && candidate.trim());
13182    return value ?value.trim() : '';
13183  }
13184  commercialProfilePhotoUrl(user) {
13185    const candidates = [user?.profilePhotoUrl, user?.photoUrl, user?.photoURL, user?.avatarUrl, user?.profilePhoto, user?.photo];
13186    return candidates.find(value => {
13187      if (typeof value !== 'string' || !value.trim()) return false;
13188      try {
13189        const host = new URL(value.trim(), 'https://rosgan.app').hostname.toLowerCase();
13190        return !['googleusercontent.com', 'ggpht.com', 'google.com'].some(domain => host === domain || host.endsWith('.' + domain));
13191      } catch { return false; }
13192    })?.trim() || '';
13193  }
13194  commercialAvatar(user, consignatariaRef, photoFailed = false) {
13195    const photo = photoFailed ? '' : this.commercialProfilePhotoUrl(user?.profile ? { ...user, ...user.profile } : user);
13196    const whiteLogo = this.consWhiteLogo(consignatariaRef);
13197    const avatar = photo || whiteLogo || this.consLogo(consignatariaRef);
13198    return { avatar, hasAvatar: !!avatar, noAvatar: !avatar, photo: !!photo,
13199      bg: photo ? '#fff' : whiteLogo ? this.consBrandColor(consignatariaRef) : '#edf3f8',
13200      size: photo ? '100%' : '22px', fit: photo ? 'cover' : 'contain' };
13201  }
13202  async fetchRosganRepresentative(lot) {
13203    if (!lot?.certifier?.uid || !this.state.currentRemate?.__rosganActive) return;
13204    const key = this.remateRouteId(this.state.currentRemate) + ':' + this.remateLotRouteId(lot);
13205    try {
13206      const result = await this.apiFetch('/api/rosgan-representative/' + key.split(':').map(encodeURIComponent).join('/'));
13207      this.setState({ rosganRepresentativeContacts: { ...this.state.rosganRepresentativeContacts, [key]: result.contact || null } });
13208    } catch { /* Keep the contact projection already supplied by the lot. */ }
13209  }
13210  rosganLotRepresentative(lot) {
13211    const key = this.remateRouteId(this.state.currentRemate) + ':' + this.remateLotRouteId(lot || {});
13212    const resolved = this.state.rosganRepresentativeContacts?.[key];
13213    const contact = resolved?.uid === lot?.certifier?.uid ? resolved : lot?.certifier;
13214    const selected = lot && Object.hasOwn(lot, 'certifiedBy') ? lot.certifiedBy : contact?.uid;
13215    if (!selected || !contact?.uid || String(selected) !== String(contact.uid)) return null;
13216    const name = String(contact.name || contact.displayName || '').trim();
13217    if (!name) return null;
13218    const phone = String(contact.phone || contact.phoneNumber || contact.whatsappPhone || '').trim();
13219    const whatsapp = String(contact.whatsappPhone || phone).replace(/\D/g, '');
13220    const dial = phone.replace(/[^\d+]/g, '');
13221    return { ...contact, name, phone, hasPhone: /\d/.test(dial), hasWhatsapp: !!whatsapp,
13222      phoneHref: /\d/.test(dial) ? 'tel:' + dial : '',
13223      whatsappHref: whatsapp ? 'https://wa.me/' + whatsapp + '?text=' + encodeURIComponent('Hola, quisiera consultar por el lote ' + (lot.lotNumber || lot.lotCert || lot.cert || '') + ' de Rosgan.') : '',
13224      noContact: !/\d/.test(dial) && !whatsapp };
13225  }
13226  openProfilePhotoViewer(event) {
13227    event?.stopPropagation?.();
13228    event?.preventDefault?.();
13229    const profilePhoto = !this.state.profilePhotoFailed ?this.profilePhotoUrl() : '';
13230    const consignatariaRef = this.state.userConsignataria?.id || this.state.userConsignataria?.slug || this.state.userConsignataria?.ref || this.state.userConsignataria?.name || '';
13231    if (!profilePhoto && !this.consLogo(consignatariaRef)) return;
13232    this.setState({ profilePhotoViewerOpen: true });
13233  }
13234  closeProfilePhotoViewer() { this.setState({ profilePhotoViewerOpen: false }); }
13235  markProfilePhotoFailed() { this.setState({ profilePhotoFailed: true, profilePhotoViewerOpen: false }); }
13236  async openProfileEditor() {
13237    const profile = this.profileApiData();
13238    const displayParts = String(this.state.user?.displayName || '').trim().split(/\s+/).filter(Boolean);
13239    this._profilePhotoFile = null;
13240    this.releaseProfileCrop();
13241    if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview);
13242    this.setState({
13243      profileEditorOpen: true,
13244      profileEditorSaving: false,
13245      profileEditorError: '',
13246      profilePhotoPreview: '',
13247      profileCropOpen: false,
13248      profileCropSource: '',
13249      profileCropZoom: 1,
13250      profileCropX: 50,
13251      profileCropY: 50,
13252      profileEditorForm: {
13253        cuitCuil: this.apiUserCuit(profile) || '',
13254        firstName: profile.firstName || profile.nombre  || displayParts[0] || '',
13255        lastName: profile.lastName || profile.apellido  || displayParts.slice(1).join(' '),
13256        phoneNumber: this.state.user?.phoneNumber || profile.phoneNumber || profile.phone  || profile.telefono || '',
13257      },
13258    });
13259    const uid=this.state.user?.uid;
13260    try {const response=await this.cabanaApiFetch('/api/users/me');const me=response?.data ?? response;if(this.state.user?.uid === uid && this.state.profileEditorOpen) this.setState({cabanaPrebidProfile:me,profileEditorForm:{...this.state.profileEditorForm,cuitCuil:me.cuitCuil || ''}});}
13261    catch(error){if(this.state.user?.uid === uid && this.state.profileEditorOpen)this.setState({profileEditorError:'No se pudo consultar el CUIT/CUIL: '+(error?.data?.detail || error.message)});}
13262
13263  }
13264  closeProfileEditor() {
13265    if (this.state.profileEditorSaving) return;
13266    this.releaseProfileCrop();
13267    if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview);
13268    this._profilePhotoFile = null;
13269    this.setState({ profileEditorOpen: false, profileEditorError: '', profilePhotoPreview: '', profileCropOpen: false, profileCropSource: '' });
13270  }
13271  setProfileEditorField(field, value) {
13272    this.setState({ profileEditorForm: { ...(this.state.profileEditorForm || {}), [field]: value }, profileEditorError: '' });
13273  }
13274  selectProfilePhoto(file) {
13275    if (!file) return;
13276    if (!/^image\/(png|jpe || g|webp)$/i.test(file.type || '')) {
13277      this.setState({ profileEditorError: 'La foto debe ser PNG, JPG o WebP.' });
13278      return;
13279    }
13280    if (file.size > 5 * 1024 * 1024) {
13281      this.setState({ profileEditorError: 'La foto no puede superar 5 MB.' });
13282      return;
13283    }
13284    this.releaseProfileCrop();
13285    const source = URL.createObjectURL(file);
13286    const image = new Image();
13287    this._profileCropFile = file;
13288    image.onload = () => {
13289      this._profileCropImage = image;
13290      this.setState({
13291        profileCropOpen: true,
13292        profileCropSource: source,
13293        profileCropZoom: 1,
13294        profileCropX: 50,
13295        profileCropY: 50,
13296        profileEditorError: '',
13297      }, () => requestAnimationFrame(() => this.drawProfileCrop()));
13298    };
13299    image.onerror = () => {
13300      URL.revokeObjectURL(source);
13301      this._profileCropFile = null;
13302      this._profileCropImage = null;
13303      this.setState({ profileEditorError: 'No pudimos abrir esa imagen.' });
13304    };
13305    image.src = source;
13306  }
13307  releaseProfileCrop() {
13308    if (this.state?.profileCropSource) URL.revokeObjectURL(this.state.profileCropSource);
13309    this._profileCropFile = null;
13310    this._profileCropImage = null;
13311  }
13312  cancelProfileCrop() {
13313    this.releaseProfileCrop();
13314    this.setState({ profileCropOpen: false, profileCropSource: '', profileCropZoom: 1, profileCropX: 50, profileCropY: 50 });
13315  }
13316  setProfileCrop(field, value) {
13317    const numericValue = Number(value);
13318    this.setState({ [field]: Number.isFinite(numericValue) ?numericValue : 0 }, () => this.drawProfileCrop());
13319  }
13320  confirmProfileCrop() {
13321    const canvas = document.querySelector('canvas[data-profile-crop-canvas="true"]');
13322    if (!canvas || !this._profileCropImage) {
13323      this.setState({ profileEditorError: 'No pudimos preparar el recorte.' });
13324      return;
13325    }
13326    this.drawProfileCrop();
13327    canvas.toBlob((blob) => {
13328      if (!blob) {
13329        this.setState({ profileEditorError: 'No pudimos preparar el recorte.' });
13330        return;
13331      }
13332      const croppedFile = new File([blob], 'foto-perfil.jpg', { type: 'image/jpeg', lastModified: Date.now() });
13333      if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview);
13334      const preview = URL.createObjectURL(croppedFile);
13335      this.releaseProfileCrop();
13336      this._profilePhotoFile = croppedFile;
13337      this.setState({
13338        profilePhotoPreview: preview,
13339        profileCropOpen: false,
13340        profileCropSource: '',
13341        profileEditorError: '',
13342      });
13343    }, 'image/jpeg', 0.92);
13344  }
13345  async saveProfileEditor() {
13346    const uid = this.state.apiUser?.uid || this.state.apiUser?.id || this.state.user?.uid;
13347    if (!uid) { this.setState({ profileEditorError: 'No pudimos identificar tu usuario.' }); return; }
13348    const form = this.state.profileEditorForm || {};
13349    const firstName = String(form.firstName || '').trim();
13350    const lastName = String(form.lastName || '').trim();
13351    const phoneNumber = String(this.state.user?.phoneNumber || form.phoneNumber || '').trim();
13352    if (form.cuitCuil && this.cuitInputRaw(form.cuitCuil).length !== 11) { this.setState({profileEditorError:'Ingresá un CUIT/CUIL de 11 dígitos.'}); return; }
13353    if (!firstName || !lastName) { this.setState({ profileEditorError: 'Completá n
13353ombre y apellido.' }); return; }
13354    this.setState({ profileEditorSaving: true, profileEditorError: '' });
13355    try {
13356      await this.apiFetch('/api/Users/' + encodeURIComponent(uid) + '/profile', {
13357        method: 'PUT',
13358        body: JSON.stringify({ firstName, lastName, phoneNumber }),
13359      });
13360      if (this._profilePhotoFile) {
13361        const photoBody = new FormData();
13362        photoBody.append('Photo', this._profilePhotoFile, this._profilePhotoFile.name);
13363        await this.apiFetch('/api/Users/' + encodeURIComponent(uid) + '/profile-photo', { method: 'POST', body: photoBody });
13364      }
13365      const firebaseUser = (await this.ensureFirebase()).auth().currentUser;
13366      if (firebaseUser?.updateProfile) await firebaseUser.updateProfile({ displayName: [firstName, lastName].filter(Boolean).join(' ') }).catch(() => {});
13367      await this.fetchApiSession();
13368      if (form.cuitCuil && this.cuitInputRaw(form.cuitCuil) !== this.cuitInputRaw(this.state.cabanaPrebidProfile?.cuitCuil)) await this.persistCabanaPrebidCuit(form.cuitCuil);
13369      if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview);
13370      this._profilePhotoFile = null;
13371      this.setState({ profileEditorOpen: false, profileEditorSaving: false, profileEditorError: '', profilePhotoPreview: '', profilePhotoFailed: false });
13372      this.showToast('Perfil actualizado.');
13373    } catch (error) {
13374      this.setState({ profileEditorSaving: false, profileEditorError: error?.message || 'No se pudo actualizar el perfil.' });
13375    }
13376  }
13377  isBasicUserProfile(user = this.state.apiUser) {
13378    return this.apiRoleIds(user).every(role => ['user', 'usuario'].includes(role));
13379  }
13380  isRosganProfile(user = this.state.apiUser) {
13381    return this.apiRoleIds(user).some((role) => ['consignatario', 'certificador'].includes(role));
13382  }
13383  canViewProfileFerias() { return this.isRosganProfile() || this.canManageUsers(this.state.apiUser); }
13384  async fetchProfileFerias() {
13385    const uid = this.state.user?.uid;
13386    if (!uid || !this.canViewProfileFerias()) return;
13387    const request = {}; this._profileFeriasRequest = request;
13388    this.setState({ profileFerias: [], profileFeriasLoading: true, profileFeriasError: '', profileFeriasUid: uid });
13389    const active = () => this._profileFeriasRequest === request && this.state.user?.uid === uid && this.canViewProfileFerias();
13390    try {
13391      const data = await this.apiFetch('/api/ferias');
13392      if (!active()) return;
13393      this.setState({ profileFerias: this.listFrom(data, ['data','items','ferias']), profileFeriasLoading: false });
13394    } catch (error) {
13395      if (active()) this.setState({ profileFeriasLoading: false, profileFeriasError: 'No se pudieron cargar los remates feria. Reintentá.' });
13396    }
13397  }
13398  profileFeriaRows() {
13399    if (!this.canViewProfileFerias() || this.state.profileFeriasUid !== this.state.user?.uid) return [];
13400    const refs = new Set((this.state.userConsignatarias || []).flatMap(c => [c.id,c.slug,c.name,c.nombre,this.consName(c)]).filter(Boolean).map(v => this.consKey(v)));
13401    const today = this.localIsoDate();
13402    return (this.state.profileFerias || []).filter(f => !f.deleted && !f.cancelled && String(f.fecha || '').slice(0,10) >= today && !['cancelled','completed','complete','finished','finalizado'].includes(String(f.status || '').toLowerCase()) && !['complete','completed'].includes(String(f.youtubeStatus || '').toLowerCase()) && (this.canManageUsers(this.state.apiUser) || [f.consignatariaId,f.consignataria].some(v => v && refs.has(this.consKey(v)))))
13403      .sort((a,b) => { const ad=String(a.fecha||''),bd=String(b.fecha||''); const ap=ad>=today,bp=bd>=today; return Number(bp)-Number(ap) || (ap?ad.localeCompare(bd):bd.localeCompare(ad)) || String(a.hora||'').localeCompare(String(b.hora||'')); })
13404      .map(f => { const date = f.fecha ? new Date(f.fecha+'T'+(f.hora||'12:00').slice(0,5)+':00-03:00') : null;
13405        const url = /^https:\/\/(?:www\.)?(?:youtube\.com\/watch\?|youtu\.be\/)/i.test(f.youtubeUrl || '') ? f.youtubeUrl : '';
13406        return { id:f.id, title:f.nombreRemate || 'Remate feria', company:f.consignataria || '', location:[f.localidad,f.provincia].filter(
13406Boolean).join(', '),
13407          date:date && !isNaN(date) ? date.toLocaleDateString('es-AR',{day:'numeric',month:'long',year:'numeric'})+' · '+(f.hora||'').slice(0,5)+' h' : 'Fecha a confirmar',
13408          copyTransmission: async () => { try { await navigator.clipboard.writeText(url); this.showToast('URL de transmisión copiada'); } catch { this.showToast('No se pudo copiar el enlace. Intentá nuevamente.'); } },
13409          downloadFlyer: async (event) => {
13410            const button=event?.currentTarget;if(button){button.disabled=true;button.querySelector('span').textContent='Generando flyer…';}
13411            try {const fullLogo=this.consFullHeroLogo(f.consignataria);const {blob,data}=await window.RosganFeriaFlyer.render(f,{color:this.consBrandColor(f.consignataria),logo:fullLogo||this.consLogo(f.consignataria),mono:!!fullLogo});const objectUrl=URL.createObjectURL(blob),a=document.createElement('a');a.href=objectUrl;a.download='feria-'+data.title.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/gi,'-').toLowerCase()+'.png';document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(objectUrl),60000);}
13412            catch(error){this.showToast(error.message||'No se pudo descargar el flyer.');}
13413            finally {if(button){button.disabled=false;button.querySelector('span').textContent='Descargar flyer';}}
13414          },
13415          canControlYoutube: this.canManageUsers(), controlYoutube: () => window.RosganFeriaYoutube.open({feria:f,apiFetch:(path,opts)=>this.apiFetch(path,opts)}),
13416          youtubeMessage: f.youtubeError || 'Transmisión pendiente', reviewTransmission: () => { this.go('feria'); this.waitFeriaYoutube(f); }, deleteFeria: () => this.deleteSavedFeria(f), editFeria: () => this.editSavedFeria(f),
13417          status:f.fecha < today ? 'Finalizado' : f.fecha === today ? 'Hoy' : 'Próximo', hasLink:!!url, href:url, logo:this.consLogo(f.consignataria), hasLogo:!!this.consLogo(f.consignataria) };
13418      });
13419  }
13420  profileUpcomingRemates(remates, now = Date.now()) {
13421    return (remates || []).filter((item) => {
13422      const date = this.remateDate(item);
13423      return item.activo !== false && item.closeRemate !== true && !item.cabanasConfig && date && date.getTime() >= now;
13424    });
13425  }
13426  async fetchProfileRosganLots() {
13427    const uid = this.state.user?.uid;
13428    if (!uid || !this.isRosganProfile()) return;
13429    const request = {};
13430    this._profileRosganRequest = request;
13431    this.setState({ profileRosganLots: [], profileRosganLoading: true, profileRosganError: '', profileRosganScopeUid: uid });
13432    const active = () => this._profileRosganRequest === request && this.state.user?.uid === uid && this.isRosganProfile();
13433    try {
13434      const remates = this.profileUpcomingRemates(this.normalizeRosganRemates(await this.apiFetch('/api/Remates'))).filter((item) =>
13435        [...(item.consignatariasParticipantes || []).map((entry) => entry.consignatariaId || entry.id), item.consignatariaId].some((id) =>
13436          (this.state.userConsignatarias || []).some((member) => String(member.id) === String(id))));
13437      const ids = new Set((this.state.userConsignatarias || []).map((item) => String(item.id)));
13438      if (!ids.size) { if (active()) this.setState({ profileRosganLoading: false }); return; }
13439      const results = [];
13440      // Limit simultaneous authenticated requests; the API enforces private lot access.
13441      for (let offset = 0; offset < remates.length; offset += 4) {
13442        const batch = await Promise.allSettled(remates.slice(offset, offset + 4).map(async (remate) => {
13443          const id = this.remateRouteId(remate);
13444          const response = await this.apiFetch('/api/remates/' + encodeURIComponent(id) + '/rosgan-lots/all');
13445          return this.rosganArray(response).map((row) => ({ ...(row.data || row.lot || {}), ...row }))
13446            .filter((lot) => ids.has(String(lot.consignatariaId)))
13447            .map((lot) => ({ ...lot, profileRemate: remate }));
13448        }));
13449        if (!active()) return;
13450        results.push(...batch);
13451      }
13452      const lots = results.filter((r) => r.status === 'fulfilled').flatMap((r) => r.value);
13453      this.setState({ profileRosganLots: lots, profileRosganLoading: false,
13454        profileRosganError: results.some((r) => r.status === 'rejected') ? 'No se pudieron consultar todos los remates. Reintentá para completar la lista.' : '' });
13455    } catch (error) {
13456      if (active()) this.setState({ profileRosganLoading: false, profileRosganError: 'No se pudieron cargar los lotes de tu consignataria. Reintentá.' });
13457    }
13458  }
13459  profileRosganRows() {
13460    const uid = String(this.state.user?.uid || '');
13461    const own = (lot) =>
13461 !!uid && String(lot.createdByUid || lot.operatorUid || lot.creatorUid || lot.createdByUserId || '') === uid;
13462    if (!uid || this.state.profileRosganScopeUid !== uid) return [];
13463    return [...(this.state.profileRosganLots || [])].sort((a, b) => Number(own(b)) - Number(own(a)) ||
13464      (Date.parse(b.createdAt || '') || 0) - (Date.parse(a.createdAt || '') || 0) ||
13465      String(a.lotCert || a.id).localeCompare(String(b.lotCert || b.id), 'es', { numeric: true }))
13466      .map((source) => {
13467        const lot = this.normalizeRosganActiveLot(source, 0, source.profileRemate);
13468        return { cert: lot.cert, category: lot.nombre_categoria, heads: lot.cantidad,
13469          remate: source.profileRemate.nombre || source.profileRemate.name || source.profileRemate.titulo || 'Remate ' + this.remateRouteId(source.profileRemate),
13470          location: [source.locality, source.province].filter(Boolean).join(', ') || 'Sin localidad',
13471          status: this.rosganLotWorkflowStatus(source), own: own(source),
13472          href: this.rematePath(source.profileRemate, lot),
13473          canEdit: this.canEditRosganLot(source), edit: () => this.openRosganLotEditor(lot, source.profileRemate) };
13474      });
13475  }
13476  async fetchUserConsignataria(uid) {
13477    if (!uid) { this.setState({ userConsignataria: null, userConsignatarias: [] }); return null; }
13478    if (this._userConsignatariaPromise && this._userConsignatariaUid === uid) return this._userConsignatariaPromise;
13479    this._userConsignatariaUid = uid;
13480    const epoch = this._sessionEpoch || 0;
13481    const current = () => this.state.user?.uid === uid && epoch === (this._sessionEpoch || 0);
13482    const request = (async () => {
13483      try {
13484        const data = await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(uid));
13485        const root = data?.data  || data;
13486        const relations = Array.isArray(root) ?root : (Array.isArray(root?.items) ?root.items : [root?.item || root?.consignatariaUser || root?.membership || root]);
13487        const resolved = relations.filter((relation) => relation?.isActive === true).flatMap((relation) => {
13488          const consignataria = relation?.consignataria && typeof relation.consignataria === 'object' ?relation.consignataria : {};
13489          const ids = Array.isArray(relation?.consignatariaIds) ?relation.consignatariaIds : [];
13490          const refs = [
13491            ...ids,
13492            relation?.consignatariaId,
13493            relation?.consignatariaSlug,
13494            relation?.consignatariaName,
13495            consignataria.id,
13496            consignataria.slug,
13497            consignataria.name,
13498            consignataria.nombre,
13499            typeof relation?.consignataria === 'string' ?relation.consignataria : '',
13500          ].filter(Boolean);
13501          return refs.map((ref) => ({ id: String(ref), slug: String(ref), name: String(ref), ref: String(ref) }));
13502        }).filter((item, index, items) => items.findIndex((candidate) => this.consKey(candidate.ref) === this.consKey(item.ref)) === index);
13503        if (current()) this.setState({ userConsignataria: resolved[0] || null, userConsignatarias: resolved });
13504        return resolved;
13505      } catch (error) {
13506        if (current() && [401, 403].includes(error?.status)) this.setState({ userConsignataria: null, userConsignatarias: [] });
13507        return null;
13508      }
13509    })();
13510    this._userConsignatariaPromise = request;
13511    try { return await request; }
13512    finally { if (this._userConsignatariaPromise === request) this._userConsignatariaPromise = null; }
13513  }
13514  consignatariaRelationRefs(data) {
13515    const root = data?.data  || data;
13516    const relations = Array.isArray(root) ?root : (Array.isArray(root?.items) ?root.items : [root?.item || root?.consignatariaUser || root?.membership || root]);
13517    return relations.filter((relation) => relation?.isActive === true).flatMap((relation) => {
13518      const consignataria = relation?.consignataria && typeof relation.consignataria === 'object' ?relation.consignataria : {};
13519      const ids = Array.isArray(relation?.consignatariaIds) ?relation.consignatariaIds : [];
13520      const refs = [
13521        ...ids,
13522        relation?.consignatariaId,
13523        relation?.consignatariaSlug,
13524        relation?.consignatariaName,
13525        consignataria.id,
13526        consignataria.slug,
13527        consignataria.name,
13528        consignataria.nombre,
13529        typeof relation?.consignataria === 'string' ?relation.consignataria : '',
13530      ].filter(Boolean);
13531      return refs.map((ref) => ({ id: String(ref), slug: String(ref), name: String(ref), ref: String(ref) }));
13532    }).filter((item, index, items) => items.findIndex((candidate) => this.consKey(candidate.ref) === this.consKey(item.ref)) === index);
13533  }
13534  async fetchLotSellerConsignataria(lot) {
13535    const sellerUid = String(lot?.sellerUid || '').trim();
13536    if (!sellerUid || lot?.consignatariaId || lot?.consignatariaName) return null;
13537    const isCurrentSeller = sellerUid === String(this.state.user?.uid || '');
13538    if (Object.prototype.hasOwnProperty.call(this.state.lotSellerConsignatarias || {}, sellerUid)) {
13539      const cached = this.state.lotSellerConsignatarias[sellerUid];
13540      if (cached || !isCurrentSeller) return cached || null;
13541    }
13542    this._lotSellerConsignatariaPromises ||= {};
13543    if (this._lotSellerConsignatariaPromises[sellerUid]) return this._lotSellerConsignatariaPromises[sellerUid];
13544    const request = (async () => {
13545      let resolved = [];
13546      if (isCurrentSeller) {
13547        resolved = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(Boolean);
13548        if (!resolved.length) resolved = (await this.fetchUserConsignataria(sellerUid)) || [];
13549      } else {
13550        try {
13551          resolved = this.consignatariaRelationRefs(await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(sellerUid)));
13552        } catch (error) {
13553          if (![403, 404].includes(Number(error?.status))) console.warn('[Roslot publisher relation failed]', sellerUid, error?.message || error);
13554        }
13555      }
13556      const relation = resolved[0] || null;
13557      this.setState({ lotSellerConsignatarias: { ...(this.state.lotSellerConsignatarias || {}), [sellerUid]: relation || false } });
13558      return relation;
13559    })();
13560    this._lotSellerConsignatariaPromises[sellerUid] = request;
13561    try { return await request; }
13562    finally { delete this._lotSellerConsignatariaPromises[sellerUid]; }
13563  }
13564  apiRoleIds(user = this.state.apiUser) {
13565    return this.userRoleIds(user);
13566  }
13567  canManageBuyerAccounts(user = this.state.apiUser) {
13568    return this.canManageSellerData(user);
13569  }
13570  canAccessApp(user = this.state.apiUser) { return true; }
13571  canCreateCabanaRemate(user = this.state.apiUser) {
13572    return !!this.state.user && this.apiRoleIds(user).some(role => ['admin', 'sysadmin'].includes(role));
13573  }
13574  canAccessRoslot(user = this.state.apiUser) {
13575    return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin'].includes(role));
13576  }
13577  canAccessFeria(user = this.state.apiUser) {
13578    return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin', 'consignatario', 'certificador'].includes(role));
13579  }
13580  canAccessStatistics(user = this.state.apiUser) {
13581    return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin'].includes(role));
13582  }
13583  canAccessRoute(route, user = this.state.apiUser) {
13584    const target = String(route || '');
13585    if (target === 'rosganLotLoad') return this.canManageRosganLots(user);
13586    if (['pool', 'lot', 'load', 'roslotSummary'].includes(target)) return this.canAccessRoslot(user);
13587    if (target === 'feria') return this.canAccessFeria(user);
13588    if (target === 'mercado') return this.canAccessStatistics(user);
13589    if (target === 'cabanaStats') return this.canSeeCabanaStatsNavigation(user);
13590    if (['home', 'remates', 'remateDetail', 'rosganResults', 'cabanas', 'cabanaDetail', 'cabanaLotDetail'].includes(target)) return true;
13591    return !!this.state.user;
13592  }
13593  isSysadmin(user = this.state.apiUser) { return this.apiRoleIds(user).includes('sysadmin'); }
13594  canManageUsers(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['admin', 'sysadmin'].includes(role)); }
13595  canManageSellerData(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['seller', 'vendedor', 'consignatario', 'admin', 'sysadmin'].includes(role)); }
13596  canManageRosganLots(user = this.state.apiUser) { return !!this.state.user && this.apiRoleIds(user).some((role) => ['seller', 'consignatario', 'certificador', 'admin', 'sysadmin'].includes(role)); }
13597  isCertifierOnly(user = this.state.apiUser) { return this.apiRoleIds(user).includes('certificador') && !this.canManageSellerData(user); }
13598  canManageComitentes(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['seller', 'vendedor', 'consignatario', 'admin', 'sysadmin'].includes(role)); }
13599  canUseBuyerData(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['buyer', 'comprador', 'consignatario', 'sysadmin'].includes(role)); }
13600  canManageCabanaPrivatePages(user = this.state.apiUser) {
13601    return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin', 'consignatario', 'seller', 'cabanero'].includes(role));
13602  }
13603  canSeeCabanaStatsNavigation(user = this.state.apiUser) {
13604    return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin', 'consignatario'].includes(role));
13605  }
13606  canSeeCabanaStats(item = this.state.currentCabana, user = this.state.apiUser) {
13607    return this.canExportCabanaCatalog(item, user);
13608  }
13609  canExportCabanaCatalog(item = this.state.currentCabana, user = this.state.apiUser) {
13610    if (!this.state.user || !item) return false;
13611    const roles = this.apiRoleIds(user);
13612    if (roles.some((role) => ['admin', 'sysadmin'].includes(role))) return true;
13613    if (!roles.includes('consignatario')) return false;
13614
13615    return this.belongsToCabanaConsignataria(item);
13616  }
13617  belongsToCabanaConsignataria(item) {
13618    const membershipRefs = [...(this.state.userConsignatarias || []), this.state.userConsignataria]
13619      .filter(Boolean)
13620      .flatMap((relation) => [relation?.id, relation?.consignatariaId, relation?.slug, relation?.ref, relation?.name, relation?.nombre]);
13621    const consignataria = item?.consignataria && typeof item.consignataria === 'object' ?item.consignataria : {};
13622    const participants = Array.isArray(item?.consignatariasParticipantes) ?item.consignatariasParticipantes : [];
13623    const auctionRefs = [
13624      item?.consignatariaId, item?.consignatariaSlug, item?.consignatariaNombre, item?.consignatariaName,
13625      item?.nombre_consignatario, item?.nombre_cuenta, typeof item?.consignataria === 'string' ?item.consignataria : '',
13626      consignataria?.id, consignataria?.consignatariaId, consignataria?.slug, consignataria?.name, consignataria?.nombre,
13627      ...participants.flatMap((participant) => [
13628        participant?.consignatariaId, participant?.id, participant?.slug,
13629        participant?.consignatariaNombre, participant?.consignatariaName, participant?.name, participant?.nombre,
13630      ]),
13631    ];
13632    const membershipKeys = new Set(membershipRefs.filter(Boolean).map((value) => this.consKey(value)).filter(Boolean));
13633    const auctionKeys = new Set(auctionRefs.filter(Boolean).map((value) => this.consKey(value)).filter(Boolean));
13634    const enrichFromCatalog = (keys) => {
13635      (this.state.consignatarias || []).forEach((record) => {
13636        const refs = [record?.id, record?.consignatariaId, record?.slug, record?.name, record?.nombre]
13637          .filter(Boolean).map((value) => this.consKey(value)).filter(Boolean);
13638        if (refs.some((key) => keys.has(key))) refs.forEach((key) => keys.add(key));
13639      });
13640    };
13641    enrichFromCatalog(membershipKeys);
13642    enrichFromCatalog(auctionKeys);
13643    return [...membershipKeys].some((key) => auctionKeys.has(key));
13644  }
13645  denyCabanaPrivatePage(item = null) {
13646    const target = item ?this.normalizeCabanaAuctionRecord(item) : null;
13647    try { window.history.replaceState({}, '', target ?this.cabanaPath(target) : '/cabanas'); } catch {}
13648    this.setState({
13649      route: target ?'cabanaDetail' : 'cabanas',
13650      currentCabana: target || null,
13651      cabanaStatsOverview: false,
13652      cabanaSalesOrderOpen: false,
13653    });
13654    if (!target && !Array.isArray(this.state.cabanas)) this.fetchCabanas();
13655    this.showToast('Esta sección requiere permisos de gestión de cabañas.');
13656    window.scrollTo(0, 0);
13657    return false;
13658  }
13659  userRoleIds(user) {
13660    const aliases = {
13661      comprador: 'buyer',
13662      vendedor: 'seller',
13663      consignataria: 'consignatario',
13664      cabana: 'cabanero',
13665      'cabaña': 'cabanero',
13666      'cabañero': 'cabanero',
13667      administrador: 'admin',
13668      administrator: 'admin',
13669      'administrador del sistema': 'sysadmin',
13670      'administrador de sistema': 'sysadmin',
13671      'system administrator': 'sysadmin',
13672      'system admin': 'sysadmin',
13673      superadmin: 'sysadmin',
13674      developer: 'dev',
13675      desarrollador: 'dev',
13676      desarrollo: 'dev',
13677      invitado: 'guest',
13678      visitante: 'guest',
13679    };
13680    const raw = [
13681      user?.roleIds, user?.roleId, user?.roles, user?.role,
13682      user?.profile?.roleId, user?.profile?.roles, user?.profile?.role,
13683      user?.userData?.roleId, user?.userData?.roles, user?.userData?.role,
13684      user?.claims?.roleId, user?.claims?.roles, user?.claims?.role,
13685      user?.customClaims?.roleId, user?.customClaims?.roles, user?.customClaims?.role,
13686      user?.session?.roleId, user?.session?.roles, user?.session?.role,
13687      user?.apiSession?.roleId, user?.apiSession?.roles, user?.apiSession?.role,
13688      user?.apiSession?.user?.roleId, user?.apiSession?.user?.roles, user?.apiSession?.user?.role,
13689      user?.devAccess === true || user?.userData?.devAccess === true ?'dev' : null,
13690    ];
13691    const unpack = (value) => {
13692      if (Array.isArray(value)) return value.flatMap(unpack);
13693      if (value && typeof value === 'object') return [value.id, value.roleIds, value.roleId, value.name, value.value, value.roles].flatMap(unpack);
13694      return value == null ?[] : [value];
13695    };
13696    return [...new Set(raw.flatMap(unpack)
13697      .map((role) => String(role || '').trim().toLowerCase())
13698      .map((role) => aliases[role] || role)
13699      .filter(Boolean))];
13700  }
13701  adminRegistrationTime(user) {
13702    const raw = user?.createdAt || user?.registeredAt || user?.createdOn || user?.creationTime || user?.metadata?.creationTime;
13703    const seconds = raw?.seconds ?? raw?._seconds;
13704    const time = seconds != null ? Number(seconds) * 1000 : (typeof raw === 'number' ? raw : Date.parse(raw || ''));
13705    return Number.isFinite(time) && time > 0 ? time : null;
13706  }
13707  adminUserDirectory() {
13708    const users = this.state.adminUsers || [];
13709    const defs = [
13710      ['auction', 'Consignatarios y certificadores', u => u.roles.some(r => ['consignatario', 'certificador'].includes(r))],
13711      ['roslot', 'Roslot · compradores y vendedores', u => u.roles.some(r => ['buyer', 'seller'].includes(r))],
13712      ['admin', 'Admin', u => u.roles.includes('admin')],
13713      ['sysadmin', 'SysAdmin', u => u.roles.includes('sysadmin')],
13714      ['other', 'Otros', u => !u.roles.some(r => ['consignatario', 'certificador', 'buyer', 'seller', 'admin', 'sysadmin'].includes(r))],
13715      ['all', 'Todos los usuarios', () => true],
13716    ];
13717    const group = this.state.adminUsersGroup || 'recent';
13718    const consId = this.state.adminUsersConsignataria || '';
13719    const consMap = new Map((this.state.adminConsignatarias || []).map(c =>
13719 [String(c.id), c.name || c.nombre || String(c.id)]));
13720    const consNames = u => (u.consignatariaIds || []).map(id => consMap.get(String(id)) || 'Consignataria no disponible').join(' · ');
13721    const normalize = v => String(v || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
13722    const search = normalize(this.state.adminUsersSearch).trim();
13723    const recent = users.filter(u => u.registeredTime).sort((a,b) => b.registeredTime - a.registeredTime || a.uid.localeCompare(b.uid)).slice(0,50);
13724    const base = group === 'recent' ? recent : users.filter((defs.find(d => d[0] === group) || defs[0])[2]);
13725    const consCounts = new Map();
13726    const staff = users.filter(u => u.roles.some(r => ['consignatario','certificador'].includes(r)));
13727    for (const user of staff) for (const id of new Set((user.consignatariaIds || []).map(String))) consCounts.set(id, (consCounts.get(id) || 0) + 1);
13728    const unassigned = staff.filter(u => !(u.consignatariaIds || []).length).length;
13729    const companies = [...consMap].map(([id,label]) => ({id,label,count:consCounts.get(id) || 0})).sort((a,b) => a.label.localeCompare(b.label,'es'));
13730    let matches = base.filter(u => (!consId || (consId === '__none__' ? !(u.consignatariaIds || []).length : (u.consignatariaIds || []).map(String).includes(consId))) && (!search || normalize(u.displayName + ' ' + u.email + ' ' + consNames(u)).includes(search)));
13731    if (group !== 'recent') matches = [...matches].sort((a,b) => a.displayName.localeCompare(b.displayName,'es') || a.uid.localeCompare(b.uid));
13732    if (group === 'all' && ['newest','oldest'].includes(this.state.adminUsersSort)) matches.sort((a,b) => !a.registeredTime ? (!b.registeredTime ? a.uid.localeCompare(b.uid) : 1) : !b.registeredTime ? -1 : (this.state.adminUsersSort === 'oldest' ? a.registeredTime - b.registeredTime : b.registeredTime - a.registeredTime) || a.uid.localeCompare(b.uid));
13733    const total = matches.length, pageSize = group === 'recent' ? 10 : 20, pages = Math.max(1,Math.ceil(total / pageSize));
13734    const page = Math.min(pages,Math.max(1,Number(this.state.adminUsersPage) || 1));
13735    const rows = matches.slice((page-1)*pageSize,page*pageSize).map(u => ({...u, consignatariaSummary:consNames(u), registrationLabel:u.registeredTime ? 'Alta: ' + new Date(u.registeredTime).toLocaleDateString('es-AR') : 'Fecha de alta no disponible'}));
13736    return {rows,total,page,pages,start:total ? (page-1)*pageSize+1 : 0,end:Math.min(page*pageSize,total),missingDates:users.filter(u => !u.registeredTime).length,
13737      companies:[{id:'',label:'Todas las consignatarias',count:staff.length},...companies,{id:'__none__',label:'Sin consignataria asignada',count:unassigned}],
13738      filters:[...defs.map(([id,label,test]) => ({id,label,count:users.filter(test).length})),{id:'recent',label:'Últimos 50 registrados',count:recent.length}]};
13739  }
13740  normAdminUser(user) {
13741    if (!user) return null;
13742    const uid = String(user.uid || user.userId || user.id || '');
13743    const email = String(user.email || '').trim();
13744    if (!uid && !email) return null;
13745    const firstName = String(user.firstName || user.nombre || '').trim();
13746    const lastName = String(user.lastName || user.apellido || '').trim();
13747    const displayName = String(user.displayName || user.name || [firstName, lastName].filter(Boolean).join(' ') || email.split('@')[0] || 'Usuario').trim();
13748    return {
13749      source: user, uid, email, displayName, appDevices: user.appDevices || [], registeredTime: this.adminRegistrationTime(user),
13750      photoUrl: user.photoUrl || user.photoURL || user.profilePhotoUrl || '',
13751      roles: Array.isArray(user.roleIds) ?this.userRoleIds({ roleIds: user.roleIds }) : this.userRoleIds(user),
13752      consignatariaIds: [...new Set((Array.isArray(user.consignatariaIds) ?user.consignatariaIds : []).map(String).filter(Boolean))],
13753      consignatariaUserIsActive: user.consignatariaUserIsActive === true,
13754    };
13755  }
13756  async hydrateAdminUserAssignments(users, actorUid) {
13757    const cache = this._adminAssignmentCache || (this._adminAssignmentCache = new Map());
13758    const result = [];
13759    // Only staff have these memberships. Read sequentially and reuse successful
13760    // responses; stop on rate limiting rather than starting a request per row.
13761    for (const user of users) {
13762      if (this.state.user?.uid !== actorUid || !this.canManageUsers()) return [];
13763      if (!user.roles.some(role => ['consignatario','certificador','cabanero'].includes(role))) { result.push(user); continue; }
13764      // The directory response already includes authoritative memberships,
13765      // including an empty array for staff without assignments.
13766      if (Array.isArray(user.source?.consignatariaIds)) {
13767        const ids = user.source.consignatariaUserIsActive === false ? [] : [...new Set(user.source.consignatariaIds.map(String).filter(
13767Boolean))];
13768        cache.set(user.uid, {at:Date.now(),ids});
13769        result.push({...user,consignatariaIds:ids,consignatariaUserIsActive:user.source.consignatariaUserIsActive === true || ids.length > 0});
13770        continue;
13771      }
13772      let entry = cache.get(user.uid);
13773      if (!entry || Date.now() - entry.at >= 300000) {
13774        let response;
13775        try { response = await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(user.uid)); }
13776        catch (error) { if (error?.status !== 404) throw error; response = []; }
13777        const root = response?.data || response;
13778        const relations = Array.isArray(root) ? root : (Array.isArray(root?.items) ? root.items : [root?.item || root?.consignatariaUser || root?.membership || root]);
13779        const active = relations.filter(relation => relation && relation.isActive !== false);
13780        const ids = [...new Set(active.flatMap(relation => Array.isArray(relation.consignatariaIds) && relation.consignatariaIds.length ? relation.consignatariaIds : [relation.consignatariaId]).filter(id => id != null && String(id).trim()).map(String))];
13781        entry = {at:Date.now(),ids}; cache.set(user.uid,entry);
13782      }
13783      result.push({...user,consignatariaIds:entry.ids,consignatariaUserIsActive:entry.ids.length > 0});
13784    }
13785    return result;
13786  }
13787  async fetchAdminUsers(force = false) {
13788    if (!this.canManageUsers() || this.state.adminUsersLoading) return;
13789    const uid = this.state.user?.uid || '';
13790    const now = Date.now();
13791    if (this._adminUsersRequestUid !== uid) {
13792      this._adminUsersRequestUid = uid;
13793      this._adminAssignmentCache = new Map();
13794      this._adminUsersLoadedAt = 0;
13795      this._adminUsersRetryAt = 0;
13796      this._adminUsersRateLimits = 0;
13797    }
13798    if (now < (this._adminUsersRetryAt || 0)) {
13799      const seconds = Math.ceil((this._adminUsersRetryAt - now) / 1000);
13800      this.setState({adminUsersError:'El servicio está limitando las consultas. Podés volver a actualizar en ' + seconds + ' segundos.'});
13801      return;
13802    }
13803    if (!force && Array.isArray(this.state.adminUsers) && now - (this._adminUsersLoadedAt || 0) < 300000) return;
13804    if (force && now - (this._adminUsersLoadedAt || 0) < 15000) return;
13805    this.setState({ adminUsersLoading: true, adminUsersError: '' });
13806    try {
13807      const directory = await this.apiFetch('/api/admin-directory' + (force ? '?refresh=1' : ''));
13808      const raw = this.listFrom(directory, ['data','items','users']);
13809      const devAccess = {uids:raw.filter(user=>(user.roleIds || []).includes('dev')).map(user=>user.uid)};
13810      const consignatariasResponse = directory.consignatarias || [];
13811      if (this.state.user?.uid !== uid || !this.canManageUsers()) return;
13812      const devUids = new Set(Array.isArray(devAccess?.uids) ?devAccess.uids.map(String) : []);
13813      let users = raw.map((user) => {
13814        const account = this.normAdminUser(user);
13815        if (!account || !devUids.has(account.uid)) return account;
13816        return { ...account, roles: [...new Set([...(account.roles || []), 'dev'])] };
13817      }).filter(Boolean)
13818        .filter((user) => user.uid !== this.state.user?.uid)
13819        .sort((a, b) => a.displayName.localeCompare(b.displayName, 'es'));
13820      users = await this.hydrateAdminUserAssignments(users, uid);
13821      if (this.state.user?.uid !== uid || !this.canManageUsers()) return;
13822      const consignatarias = this.listFrom(consignatariasResponse, ['data', 'items', 'consignatarias'])
13823        .filter((item) => item?.id && item.isActive !== false);
13824      this._adminUsersLoadedAt = Date.now();
13825      this._adminUsersRateLimits = 0;
13826      this._adminUsersRetryAt = 0;
13827      this.setState({ adminUsers: users, adminCurrentVersion: directory.currentVersion || '', adminVersionsAvailable: directory.versionsAvailable !== false, adminConsignatarias: consignatarias, adminUsersLoading: false, adminUsersError: directory.stale ? 'Mostrando la última lista disponible mientras se recupera la actualización.' : '' });
13828    } catch (error) {
13829      if (this.state.user?.uid !== uid || !this.canManageUsers()) return;
13830      let message = error?.message || 'No se pudieron cargar los usuarios.';
13831      if (error?.status === 429) {
13832        this._adminUsersRateLimits = (this._adminUsersRateLimits || 0) + 1;
13833        const wait = Math.max(Number(error.retryAfterMs) || 0, Math.min(300000,60000 * 2 ** Math.min(this._adminUsersRateLimits-1,3)));
13834        this._adminUsersRetryAt = Date.now() + wait;
13835        message = 'El servicio está limitando las consultas. Esperá ' + Math.ceil(wait / 1000) + ' segundos antes de actualizar.';
13836      }
13837      if (this.state.adminUsers?.length) message += ' Se mantiene la última lista cargada.';
13838      this.setState({ adminUsersLoading: false, adminUsersError: message });
13839    }
13840  }
13841  toggleAdminUsers() {
13842    if (!this.canManageUsers()) return;
13843    const open = !this.state.adminUsersOpen;
13844    this.setState({ adminUsersOpen: open, mailingOpen: open ?false : this.state.mailingOpen, adminUserMessage: null });
13845    if (open) this.fetchAdminUsers();
13846  }
13847  setAdminUsersSearch(value) { this.setState({ adminUsersSearch: value || '', adminUsersPage: 1 }); }
13848  async selectAdminUser(account) {
13849    if (!account?.uid || this.state.adminUserSaving || !this.canManageUsers()) return;
13850    if (this.state.adminSelectedUser?.uid === account.uid && this.state.adminUserRelationLoading) return;
13851    const actorUid = this.state.user?.uid;
13852    const cached = this._adminUsersRequestUid === actorUid ? this._adminAssignmentCache?.get(account.uid) : null;
13853    const fresh = cached && Date.now() - cached.at < 300000;
13854    const ids = [...(account.consignatariaIds || [])];
13855    this.setState({
13856      adminSelectedUser: account,
13857      adminSelectedRoles: [...(account.roles || [])],
13858      adminSelectedConsignatariaIds: ids,
13859      adminOriginalRoles: [...(account.roles || [])],
13860      adminOriginalConsignatariaIds: ids,
13861      adminOriginalRelationIsActive: account.consignatariaUserIsActive === true,
13862      adminUserRelationLoading: !fresh,
13863      adminUserMessage: null,
13864    });
13865    if (fresh) { this.setState({adminSelectedConsignatariaIds:[...cached.ids],adminOriginalConsignatariaIds:[...cached.ids],adminOriginalRelationIsActive:cached.ids.length > 0}); return; }
13866    try {
13867      const response = await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(account.uid));
13868      const root = response?.data || response;
13869      const relations = Array.isArray(root) ?root : (Array.isArray(root?.items) ?root.items : [root?.item || root?.consignatariaUser || root?.membership || root]);
13870      const assignedIds = [...new Set(relations.flatMap((relation) =>
13871        Array.isArray(relation?.consignatariaIds) && relation.consignatariaIds.length
13872          ?relation.consignatariaIds : [relation?.consignatariaId]
13873      ).map(String).filter((id) => id && id !== 'undefined'))];
13874      if (this.state.user?.uid !== actorUid || !this.canManageUsers()) return;
13875      (this._adminAssignmentCache || (this._adminAssignmentCache = new Map())).set(account.uid,{at:Date.now(),ids:assignedIds});
13876      const isActive = relations.some((relation) => relation?.isActive === true);
13877      if (this.state.user?.uid === actorUid && this.state.adminSelectedUser?.uid === account.uid) this.setState({
13878        adminSelectedConsignatariaIds: assignedIds,
13879        adminOriginalConsignatariaIds: assignedIds,
13880        adminOriginalRelationIsActive: isActive,
13881        adminUserRelationLoading: false,
13882      });
13883    } catch (error) {
13884      if (this.state.user?.uid === actorUid && this.state.adminSelectedUser?.uid === account.uid) this.setState({
13885        adminUserRelationLoading: false,
13886        ...(error?.status !== 404 ?{ adminUserMessage: { ok: false, text: 'No se pudieron consultar las asignaciones: ' + (error?.message || 'Error de conexión.') } } : {}),
13887      });
13888    }
13889  }
13890  toggleAdminRole(roleId) {
13891    if (this.state.adminUserSaving || this.state.adminUserRelationLoading) return;
13892    const id = String(roleId || '').toLowerCase();
13893    const selected = [...(this.state.adminSelectedRoles || [])];
13894    const next = id === 'user'
13895      ?(selected.includes('user') ?[] : ['user'])
13896      : [...selected.filter((role) => role !== 'user' && role !== id), ...(selected.includes(id) ?[] : [id])];
13897    this.setState({ adminSelectedRoles: next, adminUserMessage: null });
13898  }
13899  selectAdminConsignataria(id) {
13900    if (this.state.adminUserSaving || this.state.adminUserRelationLoading) return;
13901    const value = String(id || '');
13902    if (!value) return;
13903    const selected = this.state.adminSelectedConsignatariaIds || [];
13904    const next = selected.includes(value) ?selected.filter((item) => item !== value) : [...selected, value];
13905    this.setState({ adminSelectedConsignatariaIds: next, adminUserMessage: null });
13906  }
13907  async adminAccessWrite(path, options) {
13908    if (!this.state.user?.uid || !this.state.adminSelectedUser?.uid || !this.canManageUsers()) throw new Error('Iniciá sesión con permisos de administración.');
13909    if (!['PUT','DELETE'].includes(options?.method)) throw new Error('Operación de permisos no válida.');
13910    return this.apiFetch(path, options);
13911  }
13912  async saveAdminUserAccess() {
13913    const account = this.state.adminSelectedUser;
13914    if (!account?.uid || this.state.adminUserSaving || this.state.adminUserRelationLoading || !this.canManageUsers()) return;
13915    const roles = [...new Set((this.state.adminSelectedRoles || []).map((role) => String(role).trim().toLowerCase()).filter(Boolean))];
13916    if (!roles.length) {
13917      this.setState({ adminUserMessage: { ok: false, text: 'Seleccioná al menos un rol.' } });
13918      return;
13919    }
13920    const needsConsignataria = roles.some((role) => ['consignatario', 'certificador'].includes(role));
13921    const consignatariaIds = [...new Set((this.state.adminSelectedConsignatariaIds || []).map(String).filter(Boolean))];
13922    const activeIds = new Set((this.state.adminConsignatarias || []).filter((item) => item.isActive !== false).map((item) => String(item.id)));
13923    if (needsConsignataria && (!consignatariaIds.length || consignatariaIds.some((id) => !activeIds.has(id)))) {
13924      this.setState({ adminUserMessage: { ok: false, text: 'Elegí al menos una consignataria activa de la lista.' } });
13925      return;
13926    }
13927    const originalRoles = [...new Set((this.state.adminOriginalRoles || []).map((role) => String(role).trim().toLowerCase()).filter(Boolean))];
13928    const rolesChanged = roles.slice().sort().join('|') !== originalRoles.slice().sort().join('|');
13929    const backendRoles = roles.filter((role) => role !== 'dev');
13930    const originalBackendRoles = originalRoles.filter((role) => role !== 'dev');
13931    const backendRolesChanged = backendRoles.slice().sort().join('|') !== originalBackendRoles.slice().sort().join('|');
13932    const devChanged = roles.includes('dev') !== originalRoles.includes('dev');
13933    const originalConsignatariaIds = [...new Set((this.state.adminOriginalConsignatariaIds || []).map(String).filter(Boolean))];
13934    const consignatariaChanged = (needsConsignataria ?consignatariaIds : []).slice().sort().join('|') !== originalConsignatariaIds.slice().sort().join('|')
13935      || (needsConsignataria && !this.state.adminOriginalRelationIsActive);
13936    if (!rolesChanged && !consignatariaChanged) {
13937      this.setState({ adminUserMessage: { ok: true, text: 'No hay cambios para guardar.' } });
13938      return;
13939    }
13940    const checkpoint = patch => {
13941      const updated = {...this.state.adminSelectedUser,...patch};
13942      this.setState({adminSelectedUser:updated,adminUsers:(this.state.adminUsers || []).map(user=>user.uid===account.uid?updated:user),
13943        ...(patch.roles ? {adminOriginalRoles:patch.roles} : {}),
13944        ...(patch.consignatariaIds ? {adminOriginalConsignatariaIds:patch.consignatariaIds,adminOriginalRelationIsActive:patch.consignatariaUserIsActive} : {})});
13945    };
13946    this.setState({ adminUserSaving: true, adminUserRetrying:false, adminUserMessage: null });
13947    try {
13948      if (backendRolesChanged && backendRoles.length) {
13949        await this.adminAccessWrite('/api/Users/' + encodeURIComponent(account.uid) + '/role', { method: 'PUT', body: JSON.stringify({ roleId: backendRoles }) });
13950        checkpoint({roles:[...backendRoles,...(originalRoles.includes('dev')?['dev']:[])]});
13951      }
13952      if (devChanged) {
13953        await this.adminAccessWrite('/api/dev-access/' + encodeURIComponent(account.uid), {
13954          method: 'PUT',
13955          body: JSON.stringify({ enabled: roles.includes('dev') }),
13956        });
13957        checkpoint({roles});
13958      }
13959      if (consignatariaChanged) {
13960        try {
13961          await this.adminAccessWrite('/api/consignataria-users/' + encodeURIComponent(account.uid), needsConsignataria
13962            ?{ method: 'PUT', body: JSON.stringify({ consignatariaIds, isActive: true }) }
13963            : { method: 'DELETE' });
13964        } catch (relationError) {
13965          if (!(relationError?.status === 404 && !needsConsignataria)) throw relationError;
13966        }
13967      }
13968      if (consignatariaChanged) checkpoint({consignatariaIds:needsConsignataria?consignatariaIds:[],consignatariaUserIsActive:needsConsignataria});
13969      const consignataria = (this.state.adminConsignatarias || []).find((item) => String(item.id) === consignatariaIds[0]);
13970      let notificationWarning = '';
13971      try {
13972        await this.apiFetch('/api/notifications/admin-role-change', {
13973          method: 'POST',
13974          body: JSON.stringify({
13975            recipientUid: account.uid,
13976            roles,
13977            previousRoles: originalRoles,
13978            consignatariaId: needsConsignataria ?consignatariaIds[0] : '',
13979            consignatariaIds: needsConsignataria ?consignatariaIds : [],
13980            consignatariaName: needsConsignataria ?String(consignataria?.name || consignataria?.nombre || consignatariaIds[0]) : '',
13981            eventId: `roles-${account.uid}-${Date.now()}`,
13982          }),
13983        });
13984      } catch (notificationError) {
13985        notificationWarning = ' No se pudo enviar la notificacion.';
13986        console.warn('role change notification', notificationError);
13987      }
13988      if (account.uid === this.state.user?.uid) { await (await this.ensureFirebase()).auth().currentUser?.getIdToken(true); await this.refreshProfileData(true); }
13989      (this._adminAssignmentCache || (this._adminAssignmentCache = new Map())).set(account.uid,{at:Date.now(),ids:needsConsignataria?consignatariaIds:[]});
13990      const updated = { ...account, roles, consignatariaIds: needsConsignataria ?consignatariaIds : [], consignatariaUserIsActive: needsConsignataria };
13991      this.setState({
13992        adminUsers: (this.state.adminUsers || []).map((user) => user.uid === account.uid ?updated : user),
13993        adminSelectedUser: updated,
13994        adminOriginalRoles: roles,
13995        adminOriginalConsignatariaIds: updated.consignatariaIds,
13996        adminOriginalRelationIsActive: updated.consignatariaUserIsActive,
13997        adminUserSaving: false, adminUserRetrying:false,
13998        adminUserMessage: { ok: true, text: 'Roles y consignataria actualizados.' + notificationWarning },
13999      });
14000    } catch (error) {
14001      const statusHint = error?.status === 401 ?'Sesión vencida (401). ' : error?.status === 403 ?'Sin permisos (403). ' : '';
14002      this.setState({ adminUserSaving: false, adminUserRetrying:false, adminUserMessage: { ok: false, text: statusHint + (error?.message || 'No se pudieron guardar los permisos.') } });
14003    }
14004  }
14005  syncMailingRoute(prevState) {
14006    // StreamableLogic's host calls componentDidUpdate with props only, not prevState.
14007    // Keep our own snapshot and advance it BEFORE setState to avoid reset loops.
14008    prevState = this._mailingRouteSnapshot;
14009    this._mailingRouteSnapshot = { route: this.state.route, authReady: this.state.authReady, user: { uid: this.state.user?.uid }, apiUser: this.state.apiUser };
14010    const identityChanged = prevState?.user?.uid !== this.state.user?.uid;
14011    if (identityChanged) {
14012      this._mailingFetchRevision = (this._mailingFetchRevision || 0) + 1;
14013      this._mailingFetchOwner = null;
14014      this._lastMailingFetchAt = 0;
14015      this.setState({ mailingLoading: false, mailingContactsState: 'idle', mailingCampaignsState: 'idle', mailingStatusState: 'idle', mailingLaunchState: 'idle', mailingCampaigns: [], mailingContacts: [], mailingContactTotal: 0, mailingLaunchInterestTotal: 0, mailingStatus: null, mailingError: '' });
14016    }
14017    if (this.state.route === 'mailingAdmin' && this.state.authReady && this.state.user?.uid && this.canManageUsers()
14018      && (prevState?.route !== this.state.route || prevState?.authReady !== this.state.authReady || identityChanged || prevState?.apiUser !== this.state.apiUser)) {
14019      this.fetchMailing();
14020    }
14021  }
14022  mailingRoot(payload) { return payload?.data || payload || {}; }
14023  async fetchMailing(force = false) {
14024    if (!this.canManageUsers()) return;
14025    const owner = String(this.state.user?.uid || this.state.apiUser?.uid || '');
14026    if (this._mailingFetchOwner === owner && this.state.mailingLoading) return;
14027    if (!force && this._mailingFetchOwner === owner && this._lastMailingFetchAt && Date.now() - this._lastMailingFetchAt < 30000) {
14028      this.ensureMailingEditorObserver();
14029      return;
14030    }
14031    const changedOwner = this._mailingFetchOwner !== owner;
14032    this._mailingFetchOwner = owner;
14033    const revision = this._mailingFetchRevision = (this._mailingFetchRevision || 0) + 1;
14034    const current = () => revision === this._mailingFetchRevision && owner === String(this.state.user?.uid || this.state.apiUser?.uid || '') && this.canManageUsers();
14035    this.ensureMailingEditorObserver();
14036    this.setState({ mailingLoading: true, mailingError: '', mailingContactsState: 'loading', mailingCampaignsState: 'loading', mailingStatusState: 'loading', mailingLaunchState: 'loading',
14037      ...(changedOwner ? { mailingCampaigns: [], mailingStatus: null, mailingContactTotal: 0, mailingLaunchInterestTotal: 0 } : {}) });
14038    const errors = [];
14039    const load = async (url, stateKey, label, map) => {
14040      try {
14041        const data = this.mailingRoot(await this.apiFetch(url));
14042        if (current()) this.setState({ ...map(data), [stateKey]: 'ready' });
14043      } catch (error) {
14044        errors.push(label + ': ' + (error?.message || 'No se pudo consultar.'));
14045        if (current()) this.setState({ [stateKey]: 'error', mailingError: errors.join(' ') });
14046      }
14047    };
14048    await Promise.all([
14049      load('/api/mailing/contacts?summary=1', 'mailingContactsState', 'Contactos', data => ({ mailingContactTotal: Number(data.total) || 0 })),
14050      load('/api/mailing/campaigns', 'mailingCampaignsState', 'Campañas', data => ({ mailingCampaigns: Array.isArray(data.campaigns) ? data.campaigns : [] })),
14051      load('/api/mailing/status', 'mailingStatusState', 'Mailgun', data => ({ mailingStatus: data })),
14052      load('/api/launch-interests', 'mailingLaunchState', 'Interesados', data => ({ mailingLaunchInterestTotal: Number(data.total) || 0 })),
14053    ]);
14054    if (current()) {
14055      this.setState({ mailingLoading: false });
14056      this._lastMailingFetchAt = errors.length ? 0 : Date.now();
14057    }
14058  }
14059  toggleMailing() {
14060    if (!this.canManageUsers()) return;
14061    const leaving = this.state.route === 'mailingAdmin';
14062    document.documentElement.classList.remove('mailing-editor-is-expanded');
14063    this.syncAppLocation(leaving ?'profile' : 'mailingAdmin');
14064    this.setState({
14065      route: leaving ?'profile' : 'mailingAdmin',
14066      mailingOpen: !leaving,
14067      adminUsersOpen: false,
14068      mailingMessage: null,
14069      mailingEditorExpanded: false,
14070      mailingInsertDialog: '',
14071    }, () => {
14072      window.scrollTo(0, 0);
14073      if (leaving) this.refreshProfileData();
14074      else this.fetchMailing();
14075    });
14076  }
14077  toggleMailingConsent() {
14078    if (!this.canManageUsers() || this.state.mailingBusy) return;
14079    this.setState({ mailingConsentConfirmed: !this.state.mailingConsentConfirmed, mailingMessage: null });
14080  }
14081  newMailingCampaign() {
14082    this.setState({
14083      mailingSelectedId: '',
14084      mailingDraft: { name: '', subject: '', previewText: '', html: '', text: '', audience: 'all', campaignType: 'standard' },
14085      mailingReport: null,
14086      mailingReportError: '',
14087      mailingReportSearch: '',
14088      mailingReportStatusFilter: 'all',
14089      mailingReportLetter: 'all',
14090      mailingReportPage: 1,
14091      mailingMessage: null,
14092    }, () => this.syncMailingEditor(''));
14093  }
14094  launchMailingTemplate() {
14095    const html = `
14096<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="width:100%;margin:0;background:#eef4f8;font-family:Arial,Helvetica,sans-serif;color:#102f4e;">
14097  <tr><td align="center" style="padding:28px 12px;">
14098    <table role="presentation" width="680" cellspacing="0" cellpadding="0" border="0" style="width:100%;max-width:680px;background:#ffffff;border-radius:24px;overflow:hidden;box-shadow:0 16px 48px rgba(0,35,70,.13);">
14099      <tr><td style="height:7px;background:linear-gradient(90deg,#003b71 0%,#0f73a9 54%,#26b7bc 100%);font-size:0;line-height:0;">&nbsp;</td></tr>
14100      <tr><td align="center" style="padding:34px 42px 26px;background:#ffffff;">
14101        <img src="https://rosgan.app/assets/rosgan-logo-blue.png" width="190" alt="Rosgan" style="display:block;width:190px;max-width:62%;height:auto;border:0;">
14102      </td></tr>
14103      <tr><td style="padding:0 26px;">
14104        <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#003b71;border-radius:22px;overflow:hidden;">
14105          <tr><td align="center" style="padding:50px 42px 46px;background:linear-gradient(135deg,#00315f 0%,#075d98 58%,#1da8b5 100%);">
14106            <div style="font-size:12px;line-height:1.4;letter-spacing:3px;text-transform:uppercase;color:#9debf0;font-weight:800;">Rosgan.app ya est&aacute; en l&iacute;nea</div>
14107            <h1 style="margin:15px 0 16px;font-size:42px;line-height:1.08;color:#ffffff;font-weight:850;letter-spacing:-1.2px;">Todo el mercado ganadero, en un solo lugar.</h1>
14108            <p style="max-width:520px;margin:0 auto;font-size:18px;line-height:1.55;color:#dceef7;">Hoy presentamos la nueva plataforma de Rosgan: una experiencia digital m&aacute;s simple, cercana y potente para conectar personas, oportunidades y negocios.</p>
14109          </td></tr>
14110        </table>
14111      </td></tr>
14112      <tr><td style="padding:38px 42px 10px;">
14113        <p style="margin:0 0 12px;font-size:17px;line-height:1.6;color:#294b68;">Te anotaste para enterarte antes que nadie. <strong style="color:#003b71;">Ese momento lleg&oacute;: rosgan.app ya est&aacute; disponible.</strong></p>
14114        <p style="margin:0;font-size:17px;line-height:1.6;color:#294b68;">Creamos un espacio donde el mercado ganadero se encuentra, se informa y hace negocios:</p>
14115      </td></tr>
14116      <tr><td style="padding:18px 42px 8px;">
14117        <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
14118          <tr>
14119            <td width="33.33%" valign="top" style="padding:6px;">
14120              <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="height:100%;background:#f0f7fb;border:1px solid #d8e8f1;border-radius:16px;"><tr><td valign="top" style="padding:22px 18px;">
14121                <div style="width:38px;height:38px;line-height:38px;text-align:center;border-radius:12px;background:#dcedf8;color:#075d98;font-size:20px;font-weight:900;">01</div>
14122                <h2 style="margin:16px 0 8px;font-size:17px;line-height:1.25;color:#003b71;">Remates y caba&ntilde;as</h2>
14123                <p style="margin:0;font-size:13px;line-height:1.55;color:#587087;">Cat&aacute;logos, lotes y transmisiones, m&aacute;s cerca y mejor organizados.</p>
14124              </td></tr></table>
14125            </td>
14126            <td width="33.33%" valign="top" style="padding:6px;">
14127              <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="height:100%;background:#eef9f7;border:1px solid #d2eae4;border-radius:16px;"><tr><td valign="top" style="padding:22px 18px;">
14128                <div style="width:38px;height:38px;line-height:38px;text-align:center;border-radius:12px;background:#d8f0eb;color:#128268;font-size:20px;font-weight:900;">
1412802</div>
14129                <h2 style="margin:16px 0 8px;font-size:17px;line-height:1.25;color:#003b71;">Roslot</h2>
14130                <p style="margin:0;font-size:13px;line-height:1.55;color:#587087;">Una nueva forma de descubrir oportunidades y participar del mercado.</p>
14131              </td></tr></table>
14132            </td>
14133            <td width="33.33%" valign="top" style="padding:6px;">
14134              <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="height:100%;background:#fff5f4;border:1px solid #f2dedb;border-radius:16px;"><tr><td valign="top" style="padding:22px 18px;">
14135                <div style="width:38px;height:38px;line-height:38px;text-align:center;border-radius:12px;background:#f8e2df;color:#d64149;font-size:20px;font-weight:900;">03</div>
14136                <h2 style="margin:16px 0 8px;font-size:17px;line-height:1.25;color:#003b71;">Datos y conexi&oacute;n</h2>
14137                <p style="margin:0;font-size:13px;line-height:1.55;color:#587087;">Informaci&oacute;n para decidir mejor y v&iacute;nculos que hacen crecer al sector.</p>
14138              </td></tr></table>
14139            </td>
14140          </tr>
14141        </table>
14142      </td></tr>
14143      <tr><td align="center" style="padding:34px 42px 42px;">
14144        <p style="margin:0 0 20px;font-size:16px;line-height:1.55;color:#294b68;">Hoy comienza una nueva etapa. Gracias por ser parte desde el comienzo.</p>
14145        <a href="https://rosgan.app" style="display:inline-block;padding:16px 30px;border-radius:12px;background:#e63746;color:#ffffff;text-decoration:none;font-size:16px;line-height:1;font-weight:850;box-shadow:0 8px 20px rgba(230,55,70,.24);">Ingresar a rosgan.app</a>
14146      </td></tr>
14147      <tr><td align="center" style="padding:23px 30px;background:#002b53;color:#bad0e2;font-size:12px;line-height:1.6;">
14148        <strong style="display:block;color:#ffffff;font-size:14px;letter-spacing:.4px;">ROSGAN</strong>
14149        El mercado ganadero, ahora conectado.
14150      </td></tr>
14151    </table>
14152  </td></tr>
14153</table>`;
14154    return {
14155      name: 'Lanzamiento rosgan.app',
14156      subject: 'Rosgan.app ya está disponible: todo el mercado ganadero en un solo lugar',
14157      previewText: 'Conocé la nueva plataforma de Rosgan. El mercado ganadero, ahora conectado.',
14158      html,
14159      text: 'Rosgan.app ya está disponible.\n\nTodo el mercado ganadero, en un solo lugar. Hoy presentamos la nueva plataforma de Rosgan: una experiencia digital más simple, cercana y potente para conectar personas, oportunidades y negocios.\n\nTe anotaste para enterarte antes que nadie. Ese momento llegó.\n\nRemates y cabañas, Roslot, información y conexiones para decidir mejor.\n\nIngresá ahora: https://rosgan.app',
14160      audience: 'launch_interest',
14161      campaignType: 'launch',
14162    };
14163  }
14164  newLaunchMailingCampaign() {
14165    const draft = this.launchMailingTemplate();
14166    this.setState({
14167      mailingSelectedId: '',
14168      mailingDraft: draft,
14169      mailingReport: null,
14170      mailingReportError: '',
14171      mailingReportSearch: '',
14172      mailingReportStatusFilter: 'all',
14173      mailingReportLetter: 'all',
14174      mailingReportPage: 1,
14175      mailingMessage: { ok: true, text: 'Plantilla de lanzamiento lista. Revisala y guardá la campaña cuando quieras.' },
14176    }, () => this.syncMailingEditor(draft.html));
14177  }
14178  selectMailingCampaign(campaign) {
14179    if (!campaign) return;
14180    this.setState({
14181      mailingSelectedId: campaign.id || '',
14182      mailingDraft: {
14183        name: campaign.name || '', subject: campaign.subject || '', previewText: campaign.previewText || '',
14184        html: campaign.html || '', text: campaign.text || '', audience: campaign.campaignType === 'launch' ?'launch_interest' : 'all', campaignType: campaign.campaignType === 'launch' ?'launch' : 'standard',
14185      },
14186      mailingReport: null,
14187      mailingReportError: '',
14188      mailingReportSearch: '',
14189      mailingReportStatusFilter: 'all',
14190      mailingReportLetter: 'all',
14191      mailingReportPage: 1,
14192      mailingMessage: null,
14193    }, () => {
14194      this.syncMailingEditor(campaign.html || '');
14195      this.fetchMailingCampaignReport(campaign.id);
14196    });
14197  }
14198  setMailingDraftField(field, value) {
14199    if (!['name', 'subject', 'previewText', 'html', 'text'].includes(field)) return;
14200    this.setState({ mailingDraft: { ...(this.state.mailingDraft || {}), [field]: value || '' }, mailingMessage: null });
14201  }
14202  mailingSelectedCampaign() {
14203    return (this.state.mailingCampaigns || []).find((campaign) => campaign.id === this.state.mailingSelectedId) || null;
14204  }
14205  mailingEditorElement() { return document.querySelector('[data-mailing-rich-editor="true"]'); }
14206  ensureMailingEditorObserver() {
14207    if (this._mailingEditorObserver || typeof MutationObserver === 'undefined') return;
14208    this._mailingEditorObserver = new MutationObserver(() => {
14209      const editor = this.mailingEditorElement();
14210      if (!editor || editor.dataset.mailingHydrated === 'true') return;
14211      editor.dataset.mailingHydrated = 'true';
14212      editor.innerHTML = this.mailingSanitizeHtml(this.state.mailingDraft?.html || '');
14213    });
14214    this._mailingEditorObserver.observe(document.body, { childList: true, subtree: true });
14215  }
14216  mailingSanitizeHtml(value) {
14217    const template = document.createElement('template');
14218    template.innerHTML = String(value || '');
14219    template.content.querySelectorAll('script,iframe,object,embed,form,input,textarea,select,option,meta,link,base').forEach((node) => node.remove());
14220    template.content.querySelectorAll('*').forEach((node) => {
14221      [...node.attributes].forEach((attribute) => {
14222        const name = attribute.name.toLowerCase();
14223        const raw = String(attribute.value || '').trim();
14224        if (name.startsWith('on')) node.removeAttribute(attribute.name);
14225        if ((name === 'href' || name === 'src') && !/^(https?:|mailto:|tel:|#|\/)/i.test(raw) && !(name === 'src' && /^data:image\/(?:png|jpe?g|gif|webp);base64,/i.test(raw))) node.removeAttribute(attribute.name);
14226        if (name === 'style' && /(?:javascript:|expression\s*\(|url\s*\(|behavior\s*:|-moz-binding)/i.test(raw)) node.removeAttribute(attribute.name);
14227      });
14228    });
14229    return template.innerHTML;
14230  }
14231  syncMailingEditor(html) {
14232    const value = this.mailingSanitizeHtml(html == null ?this.state.mailingDraft?.html : html);
14233    requestAnimationFrame(() => {
14234      const editor = this.mailingEditorElement();
14235      if (editor) {
14236        editor.dataset.mailingHydrated = 'true';
14237        if (editor.innerHTML !== value) editor.innerHTML = value;
14238      }
14239    });
14240  }
14241  captureMailingEditorHtml() {
14242    if (this.state.mailingEditorSourceMode) return this.mailingSanitizeHtml(this.state.mailingDraft?.html || '');
14243    const editor = this.mailingEditorElement();
14244    const html = this.mailingSanitizeHtml(editor?.innerHTML || this.state.mailingDraft?.html || '');
14245    this.state.mailingDraft = { ...(this.state.mailingDraft || {}), html };
14246    return html;
14247  }
14248  onMailingVisualInput(event) {
14249    if (this.mailingSelectedCampaign()?.status !== 'draft' && this.mailingSelectedCampaign()) return;
14250    const html = this.mailingSanitizeHtml(event?.currentTarget?.innerHTML || event?.target?.innerHTML || '');
14251    this.state.mailingDraft = { ...(this.state.mailingDraft || {}), html };
14252    this.saveMailingEditorSelection();
14253  }
14254  saveMailingEditorSelection() {
14255    const editor = this.mailingEditorElement();
14256    const selection = window.getSelection?.();
14257    if (!editor || !selection?.rangeCount) return;
14258    const range = selection.getRangeAt(0);
14259    if (editor.contains(range.commonAncestorContainer)) this._mailingEditorRange = range.cloneRange();
14260  }
14261  restoreMailingEditorSelection() {
14262    const editor = this.mailingEditorElement();
14263    if (!editor) return;
14264    editor.focus();
14265    if (!this._mailingEditorRange) return;
14266    const selection = window.getSelection?.();
14267    selection?.removeAllRanges();
14268    selection?.addRange(this._mailingEditorRange);
14269  }
14270  execMailingEditorCommand(event, command, value = null) {
14271    event?.preventDefault?.();
14272    const campaign = this.mailingSelectedCampaign();
14273    if (this.state.mailingEditorSourceMode || (campaign && campaign.status !== 'draft')) return;
14274    this.restoreMailingEditorSelection();
14275    document.execCommand(command, false, value);
14276    this.captureMailingEditorHtml();
14277    this.saveMailingEditorSelection();
14278  }
14279  insertMailingEditorHtml(html) {
14280    if (!html) return;
14281    this.restoreMailingEditorSelection();
14282    document.execCommand('insertHTML', false, this.mailingSanitizeHtml(html));
14283    this.captureMailingEditorHtml();
14284    this.saveMailingEditorSelection();
14285  }
14286  toggleMailingEditorSource() {
14287    if (this.state.mailingEditorSourceMode) {
14288      const html = this.mailingSanitizeHtml(this.state.mailingDraft?.html || '');
14289      this.setState({ mailingEditorSourceMode: false, mailingDraft: { ...(this.state.mailingDraft || {}), html } }, () => this.syncMailingEditor(html));
14290      return;
14291    }
14292    const html = this.captureMailingEditorHtml();
14293    this.setState({ mailingEditorSourceMode: true, mailingDraft: { ...(this.state.mailingDraft || {}), html } });
14294  }
14295  toggleMailingEditorExpanded() {
14296    const html = this.captureMailingEditorHtml();
14297    const expanded = !this.state.mailingEditorExpanded;
14298    document.documentElement.classList.toggle('mailing-editor-is-expanded', expanded);
14299    this.setState({ mailingEditorExpanded: expanded }, () => this.syncMailingEditor(html));
14300  }
14301  mailingEscapeHtml(value) {
14302    return String(value || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
14303  }
14304  mailingNormalizeUrl(value) {
14305    let url = String(value || '').trim();
14306    if (!url) return '';
14307    if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) url = 'https://' + url;
14308    return /^(https?:|mailto:|tel:)/i.test(url) ?url : '';
14309  }
14310  openMailingInsertDialog(type) {
14311    this.saveMailingEditorSelection();
14312    const selected = String(window.getSelection?.()?.toString?.() || '').trim();
14313    this.setState({ mailingInsertDialog: type, mailingInsertUrl: '', mailingInsertText: selected, mailingInsertImageSize: 'medium', mailingInsertImageLink: '', mailingImageError: '' });
14314  }
14315  closeMailingInsertDialog() {
14316    this.setState({ mailingInsertDialog: '', mailingInsertUrl: '', mailingInsertText: '', mailingInsertImageSize: 'medium', mailingInsertImageLink: '', mailingImageError: '' });
14317  }
14318  async uploadMailingEditorImage(file) {
14319    if (!file || this.state.mailingImageUploading) return;
14320    if (!/^image\/(jpeg|png|webp|gif)$/i.test(file.type || '') || file.size > 8 * 1024 * 1024) {
14321      this.setState({ mailingImageError: 'Usá una imagen JPG, PNG, WEBP o GIF de hasta 8 MB.' });
14322      return;
14323    }
14324    this.setState({ mailingImageUploading: true, mailingImageError: '' });
14325    try {
14326      const dataUrl = await new Promise((resolve, reject) => {
14327        const reader = new FileReader();
14328        reader.onload = () => resolve(String(reader.result || ''));
14329        reader.onerror = () => reject(new Error('No se pudo leer la imagen.'));
14330        reader.readAsDataURL(file);
14331      });
14332      const raw = await this.apiFetch('/api/mailing/assets/images', {
14333        method: 'POST',
14334        body: JSON.stringify({ fileName: file.name, contentType: file.type, dataBase64: dataUrl.split(',')[1] || '' }),
14335      });
14336      const image = this.mailingRoot(raw);
14337      this.setState({ mailingImageUploading: false, mailingInsertUrl: image.url || '', mailingInsertText: this.state.mailingInsertText || file.name.replace(/\.[^.]+$/, ''), mailingImageError: '' });
14338    } catch (error) {
14339      this.setState({ mailingImageUploading: false, mailingImageError: error?.message || 'No se pudo subir la imagen.' });
14340    }
14341  }
14342  confirmMailingInsert() {
14343    const type = this.state.mailingInsertDialog;
14344    const url = this.mailingNormalizeUrl(this.state.mailingInsertUrl);
14345    const text = String(this.state.mailingInsertText || '').trim();
14346    if (!type || !url) return;
14347    if (type === 'image') {
14348      if (!/^https?:/i.test(url)) return;
14349      const rawLink = String(this.state.mailingInsertImageLink || '').trim();
14350      const redirectUrl = rawLink ?this.mailingNormalizeUrl(rawLink) : '';
14351      if (rawLink && !/^https?:/i.test(redirectUrl)) return;
14352      const maxWidths = { small: '320px', medium: '480px', large: '640px', full: '100%' };
14353      const maxWidth = maxWidths[this.state.mailingInsertImageSize] || maxWidths.medium;
14354      const image = '<img src="' + this.mailingEscapeHtml(url) + '" alt="' + this.mailingEscapeHtml(text || 'Imagen') + '" style="display:block;width:100%;max-width:100%;height:auto;border:0;border-radius:8px">';
14355      const content = redirectUrl ?'<a href="' + this.mailingEscapeHtml(redirectUrl) + '" target="_blank" rel="noopener noreferrer" style="display:inline-block;width:100%;max-width:' + maxWidth + ';text-decoration:none">' + image + '</a>' : '<span style="display:inline-block;width:100%;max-width:' + maxWidth + '">' + image + '</span>';
14356      this.insertMailingEditorHtml('<div style="text-align:center;margin:18px 0">' + content + '</div>');
14357    } else if (type === 'button') {
14358      this.insertMailingEditorHtml('<div style="text-align:center;margin:22px 0"><a href="' + this.mailingEscapeHtml(url) + '" style="display:inline-block;padding:13px 22px;border-radius:8px;background:#075ca8;color:#ffffff;text-decoration:none;font-family:Arial,sans-serif;font-size:15px;font-weight:700">' + this.mailingEscapeHtml(text || 'Ver más') + '</a></div>');
14359    } else {
14360      this.insertMailingEditorHtml('<a href="' + this.mailingEscapeHtml(url) + '" style="color:#075ca8;text-decoration:underline">' + this.mailingEscapeHtml(text || url) + '</a>');
14361    }
14362    this.closeMailingInsertDialog();
14363  }
14364  previewMailingEditor() {
14365    const body = this.captureMailingEditorHtml();
14366    const popup = window.open('', '_blank');
14367    if (!popup) { this.showToast('El navegador bloqueó la vista previa.'); return; }
14368    try { popup.opener = null; } catch {}
14369    const signature = '<div style="margin-top:28px;padding-top:18px;border-top:1px solid #dce6ef;color:#233b53;font:13px/1.6 Arial,sans-serif"><strong>Mercado Ganadero S.A.</strong><br><strong>Prensa y Comunicación</strong><br>Paraguay 777, Piso 11. S2000AWV.<br>Rosario. Santa Fe. República Argentina.<br>Celular: +54 9 341 2142232<br>Tel: (54 - 341) 2858558 interno 2968<br>E-mail: [email protected]<br>www.rosgan.com.ar</div>';
14370    popup.document.open();
14371    popup.document.write('<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Vista previa del correo</title><style>body{margin:0;background:#eef3f7;font-family:Arial,sans-serif;color:#172b3f}.mail{max-width:720px;margin:28px auto;background:#fff;padding:34px;box-sizing:border-box;box-shadow:0 12px 36px rgba(0,30,60,.12)}img{max-width:100%;height:auto}@media(max-width:760px){.mail{margin:0;padding:20px}}</style></head><body><main class="mail">' + this.mailingSanitizeHtml(body) + signature + '</main></body></html>');
14372    popup.document.close();
14373  }
14374  async fetchMailingCampaignReport(campaignId) {
14375    const id = String(campaignId || this.state.mailingSelectedId || '').trim();
14376    if (!id || !this.canManageUsers()) return;
14377    this.setState({ mailingReportLoading: true, mailingReportError: '' });
14378    try {
14379      const raw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(id) + '/report?limit=500');
14380      const report = this.mailingRoot(raw);
14381      const deliveries = Array.isArray(report.deliveries) ?report.deliveries.slice() : [];
14382      let cursor = String(report.nextCursor || '');
14383      for (let page = 0; cursor && page < 200; page++) {
14384        if (this.state.mailingSelectedId !== id) return;
14385        const nextRaw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(id) + '/report?limit=500&summary=0&cursor=' + encodeURIComponent(cursor));
14386        const next = this.mailingRoot(nextRaw);
14387        deliveries.push(...(Array.isArray(next.deliveries) ?next.deliveries : []));
14388        cursor = String(next.nextCursor || '');
14389      }
14390      report.deliveries = deliveries;
14391      report.nextCursor = cursor;
14392      if (this.state.mailingSelectedId !== id) return;
14393      this.setState({ mailingReport: report, mailingReportLoading: false, mailingReportError: '', mailingReportPage: 1 });
14394    } catch (error) {
14395      if (this.state.mailingSelectedId !== id) return;
14396      this.setState({ mailingReportLoading: false, mailingReportError: error?.message || 'No se pudo cargar el reporte.' });
14397    }
14398  }
14399  mailingReportDate(value) {
14400    const date = value ?new Date(value) : null;
14401    if (!date || Number.isNaN(date.getTime())) return '—';
14402    return date.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
14403  }
14404  async exportMailingCampaignReport() {
14405    const campaign = this.mailingSelectedCampaign();
14406    if (!campaign?.id || this.state.mailingReportExporting) return;
14407    this.setState({ mailingReportExporting: true, mailingReportError: '' });
14408    try {
14409      const deliveries = [];
14410      let cursor = '';
14411      for (let page = 0; page < 200; page++) {
14412        const path = '/api/mailing/campaigns/' + encodeURIComponent(campaign.id) + '/report?limit=500' + (cursor ?'&cursor=' + encodeURIComponent(cursor) : '');
14413        const report = this.mailingRoot(await this.apiFetch(path));
14414        deliveries.push(...(Array.isArray(report.deliveries) ?report.deliveries : []));
14415        cursor = String(report.nextCursor || '');
14416        if (!cursor) break;
14417      }
14418      const columns = [
14419        ['Email', 'email'], ['Nombre', 'name'], ['Tipo', 'kind'], ['Estado', 'status'], ['Enviado', 'sentAt'],
14420        ['Entregado', 'deliveredAt'], ['Primera apertura', 'openedAt'], ['Última apertura', 'lastOpenedAt'],
14421        ['Aperturas', 'openCount'], ['Primer clic', 'clickedAt'], ['Último clic', 'lastClickedAt'],
14422        ['Clics', 'clickCount'], ['Rebote', 'failedAt'], ['Baja', 'unsubscribedAt'], ['Queja', 'complainedAt'], ['Detalle', 'error'],
14423      ];
14424      const cell = (value) => {
14425        let text = String(value == null ?'' : value).replace(/\r?\n/g, ' ');
14426        if (/^[=+\-@]/.test(text)) text = "'" + text;
14427        return '"' + text.replace(/"/g, '""') + '"';
14428      };
14429      const rows = [columns.map(([label]) => cell(label)).join(';')];
14430      deliveries.forEach((delivery) => rows.push(columns.map(([, key]) => cell(delivery[key])).join(';')));
14431      const base = String(campaign.name || campaign.subject || 'campana').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase() || 'campana';
14432      this.downloadCabanaBlob(new Blob(['\ufeff', rows.join('\r\n')], { type: 'text/csv;charset=utf-8' }), 'reporte-mailing-' + base + '.csv');
14433      this.setState({ mailingReportExporting: false });
14434    } catch (error) {
14435      this.setState({ mailingReportExporting: false, mailingReportError: error?.message || 'No se pudo exportar el reporte.' });
14436    }
14437  }
14438  async saveMailingDraft() {
14439    if (!this.canManageUsers() || this.state.mailingBusy) return;
14440    const capturedHtml = this.captureMailingEditorHtml();
14441    this.state.mailingDraft = { ...(this.state.mailingDraft || {}), html: capturedHtml };
14442    const draft = this.state.mailingDraft || {};
14443    if (!String(draft.subject || '').trim() || !String(draft.html || '').trim()) {
14444      this.setState({ mailingMessage: { ok: false, text: 'Completá el asunto y el contenido HTML.' } });
14445      return;
14446    }
14447    const selected = this.mailingSelectedCampaign();
14448    if (selected && selected.status !== 'draft') return;
14449    this.setState({ mailingBusy: true, mailingMessage: null });
14450    try {
14451      const raw = await this.apiFetch(selected ?'/api/mailing/campaigns/' + encodeURIComponent(selected.id) : '/api/mailing/campaigns', {
14452        method: selected ?'PUT' : 'POST',
14453        body: JSON.stringify(draft),
14454      });
14455      const campaign = this.mailingRoot(raw);
14456      const campaigns = selected
14457        ?(this.state.mailingCampaigns || []).map((item) =>
14457 item.id === selected.id ?campaign : item)
14458        : [campaign, ...(this.state.mailingCampaigns || [])];
14459      this.setState({ mailingBusy: false, mailingCampaigns: campaigns, mailingSelectedId: campaign.id || '', mailingDraft: { name: campaign.name || '', subject: campaign.subject || '', previewText: campaign.previewText || '', html: campaign.html || '', text: campaign.text || '', audience: campaign.campaignType === 'launch' ?'launch_interest' : 'all', campaignType: campaign.campaignType === 'launch' ?'launch' : 'standard' }, mailingMessage: { ok: true, text: 'Campaña guardada.' } }, () => this.syncMailingEditor(campaign.html || ''));
14460    } catch (error) {
14461      this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo guardar la campaña.' } });
14462    }
14463  }
14464  async sendMailingTest() {
14465    const campaign = this.mailingSelectedCampaign();
14466    if (!campaign?.id || this.state.mailingBusy) return;
14467    const email = String(this.state.mailingTestEmail || this.state.user?.email || '').trim();
14468    if (!/^\S+@\S+\.\S+$/.test(email)) {
14469      this.setState({ mailingMessage: { ok: false, text: 'Ingresá un email de prueba válido.' } });
14470      return;
14471    }
14472    this.setState({ mailingBusy: true, mailingMessage: null });
14473    try {
14474      const raw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(campaign.id) + '/test', { method: 'POST', body: JSON.stringify({ email }) });
14475      const data = this.mailingRoot(raw);
14476      const updatedCampaign = data.campaign || { ...campaign, testC
14476ount: (Number(campaign.testCount) || 0) + 1, lastTestAt: new Date().toISOString(), lastTestEmail: email };
14477      const campaigns = (this.state.mailingCampaigns || []).map((item) => item.id === campaign.id ?updatedCampaign : item);
14478      this.setState({ mailingBusy: false, mailingCampaigns: campaigns, mailingMessage: { ok: true, text: 'Prueba aceptada por Mailgun para ' + email + '. El tracking se actualizará automáticamente.' } }, () => this.fetchMailingCampaignReport(campaign.id));
14479    } catch (error) {
14480      this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo enviar la prueba.' } });
14481    }
14482  }
14483  async deleteMailingCampaign(campaign) {
14484    if (!campaign?.id || this.state.mailingBusy || ['queued', 'sending'].includes(campaign.status)) return;
14485    if (!window.confirm('¿Eliminar la campaña "' + (campaign.name || campaign.subject || 'Sin nombre') + '" y todo su reporte? Esta acción no se puede deshacer.')) return;
14486    this.setState({ mailingBusy: true, mailingMessage: null });
14487    try {
14488      await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(campaign.id), { method: 'DELETE' });
14489      const campaigns = (this.state.mailingCampaigns || []).filter((item) => item.id !== campaign.id);
14490      const selected = this.state.mailingSelectedId === campaign.id;
14491      this.setState({
14492        mailingBusy: false,
14493        mailingCampaigns: campaigns,
14494        mailingSelectedId: selected ?'' : this.state.mailingSelectedId,
14495        mailingDraft: selected ?{ name: '', subject: '', previewText: '', html: '', text: '', audience: 'all', campaignType: 'standard' } : this.state.mailingDraft,
14496        mailingReport: selected ?null : this.state.mailingReport,
14497        mailingReportError: selected ?'' : this.state.mailingReportError,
14498        mailingMessage: { ok: true, text: 'Campaña eliminada.' },
14499      }, () => { if (selected) this.syncMailingEditor(''); });
14500    } catch (error) {
14501      this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo eliminar la campaña.' } });
14502    }
14503  }
14504  async queueMailingCampaign() {
14505    const campaign = this.mailingSelectedCampaign();
14506    const resumable = ['draft', 'failed', 'cancelled'].includes(campaign?.status || '');
14507    if (!campaign?.id || !resumable || this.state.mailingBusy) return;
14508    const action = campaign.status === 'draft' ?'Enviar' : 'Reanudar';
14509    const audienceLabel = campaign.audience === 'launch_interest'
14510      ?'las personas anotadas para conocer el lanzamiento'
14511      : 'todos los contactos suscriptos';
14512    if (!window.confirm('¿' + action + ' "' + campaign.name + '"? Se incluirán ' + audienceLabel + ' que sigan vigentes y todavía no tengan un envío registrado en esta campaña.')) return;
14513    this.setState({ mailingBusy: true, mailingMessage: null });
14514    try {
14515      const raw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(campaign.id) + '/queue', { method: 'POST', body: '{}' });
14516      const response = this.mailingRoot(raw);
14517      const queuedTotal = Number(response.total) || 0;
14518      const remaining = Number(response.remaining) || 0;
14519      const campaigns = (this.state.mailingCampaigns || []).map((item) => item.id === campaign.id ?{ ...item, status: 'queued', total: queuedTotal } : item);
14520      this.setState({ mailingBusy: false, mailingCampaigns: campaigns, mailingMessage: { ok: true, text: 'Envío iniciado para ' + remaining.toLocaleString('es-AR') + ' contactos pendientes, sin repetir los ya registrados.' } });
14521    } catch (error) {
14522      this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo programar la campaña.' } });
14523    }
14524  }
14525  parseMailingCsv(text) {
14526    const lines = String(text || '').replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim());
14527    if (!lines.length) return [];
14528    const separator = (lines[0].match(/;/g) || []).length > (lines[0].match(/,/g) || []).length ?';' : ',';
14529    const parseLine = (line) => {
14530      const cells = []; let current = ''; let quoted = false;
14531      for (let i = 0; i < line.length; i++) {
14532        const char = line[i];
14533        if (char === '"' && quoted && line[i + 1] === '"') { current += '"'; i++; }
14534        else if (char === '"') quoted = !quoted;
14535        else if (char === separator && !quoted) { cells.push(current.trim()); current = ''; }
14536        else current += char;
14537      }
14538      cells.push(current.trim());
14539      return cells;
14540    };
14541    const headers = parseLine(lines[0]).map((header) => header.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim());
14542    const emailIndex = headers.findIndex((header) => ['email', 'mail', 'correo', 'correo electronico'].includes(header));
14543    if (emailIndex < 0) throw new Error('El CSV debe tener una columna email.');
14544    const nameIndex = headers.findIndex((header) => ['nombre', 'name', 'nombre completo'].includes(header));
14545    const sourceIndex = headers.findIndex((header) => ['origen', 'source'].includes(header));
14546    const statusIndex = headers.findIndex((header) => ['estado', 'status'].includes(header));
14547    const validationIndex = headers.findIndex((header) => ['validacion', 'validation status', 'validation_status', 'estado validacion', 'estado_validacion'].includes(header));
14548    const validationReasonIndex = headers.findIndex((header) => ['motivo validacion', 'motivo_validacion', 'validation reason', 'validation_reason'].includes(header));
14549    const bounceCountIndex = headers.findIndex((header) => ['rebotes', 'bounce count', 'bounce_count'].includes(header));
14550    const tagsIndex = headers.findIndex((header) => ['etiquetas', 'tags'].includes(header));
14551    return lines.slice(1).map(parseLine).map((cells) => ({
14552      email: cells[emailIndex] || '', name: nameIndex >= 0 ?cells[nameIndex] || '' : '',
14553      source: sourceIndex >= 0 ?cells[sourceIndex] || '' : 'importación CSV',
14554      status: statusIndex >= 0 ?cells[statusIndex] || '' : 'subscribed',
14555      validationStatus: validationIndex >= 0 ?cells[validationIndex] || '' : 'validated',
14556      validationReason: validationReasonIndex >= 0 ?cells[validationReasonIndex] || '' : '',
14557      bounceCount: bounceCountIndex >= 0 ?Number(cells[bounceCountIndex]) || 0 : 0,
14558      tags: tagsIndex >= 0 ?cells[tagsIndex].split('|').map((tag) =>
14558 tag.trim()).filter(Boolean) : [],
14559    })).filter((contact) => contact.email);
14560  }
14561  async importMailingCsvFile(file) {
14562    if (!file || this.state.mailingBusy) return;
14563    if (!this.state.mailingConsentConfirmed) {
14564      this.setState({ mailingMessage: { ok: false, text: 'Confirmá el consentimiento de la base antes de importarla.' } });
14565      return;
14566    }
14567    this.setState({ mailingBusy: true, mailingMessage: null });
14568    try {
14569      const contacts = this.parseMailingCsv(await file.text());
14570      let imported = 0; let rejected = 0;
14571      for (let offset = 0; offset < contacts.length; offset += 1000) {
14572        const raw = await this.apiFetch('/api/mailing/contacts/import', { method: 'POST', body: JSON.stringify({ contacts: contacts.slice(offset, offset + 1000), source: file.name, consentConfirmed: true }) });
14573        const data = this.mailingRoot(raw); imported += Number(data.imported) || 0; rejected += Number(data.rejected) || 0;
14574      }
14575      this.setState({ mailingBusy: false, mailingMessage: { ok: true, text: imported + ' contactos importados' + (rejected ?' · ' + rejected + ' rechazados.' : '.') } });
14576      await this.fetchMailing(true);
14577    } catch (error) {
14578      this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo importar el CSV.' } });
14579    }
14580  }
14581  async fetchAllPages(path, keys) {
14582    const all = [];
14583    let cursor = '';
14584    for (let page = 0; page < 100; page++) {
14585      const separator = path.includes('?') ? '&' : '?';
14586      const url = path + separator + 'limit=100' + (cursor ?'&cursor=' + encodeURIComponent(cursor) : '');
14587      const data = await this.apiFetch(url);
14588      all.push(...this.listFrom(data, keys));
14589      cursor = String(data?.nextCursor  || data?.data?.nextCursor || '');
14590      if (!cursor) break;
14591    }
14592    return all;
14593  }
14594  appPathForRoute(route) {
14595    const paths = {
14596      home: '/inicio',
14597      pool: '/roslot',
14598      roslotSummary: '/roslot/resumen-diario',
14599      load: '/roslot/cargar-lote',
14600      rosganLotLoad: '/cargar-lote-rosgan',
14601      remates: '/remates',
14602      rosganRemateCreate: '/remates/crear',
14603      cabanas: '/cabanas',
14604      feria: '/feria',
14605      feriaConfig: '/feria/config',
14606      mercado: '/estadisticas',
14607      profile: '/perfil',
14608      devActivity: '/dev/actividad',
14609      mailingAdmin: '/mailing',
14610      notifications: '/notificaciones',
14611    };
14612    return paths[route] || '';
14613  }
14614  routeFromPath(pathname = window.location.pathname) {
14615    let decoded = String(pathname || '/');
14616    try { decoded = decodeURIComponent(decoded); } catch {}
14617    const path = decoded.replace(/\/+$/, '') || '/';
14618    const normalized = path.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
14619    const parts = normalized.split('/').filter(Boolean);
14620    if (!parts.length || parts[0] === 'lacassina') return '';
14621    if (parts[0] === 'feria' && parts[1] === 'config') return 'feriaConfig';
14622    if (parts[0] === 'roslot' && parts[1] === 'resumen-diario') return 'roslotSummary';
14623    if (parts[0] === 'roslot' && parts[1] === 'cargar-lote') return 'load';
14624    if (parts[0] === 'roslot' && parts[1] === 'lotes' && parts[2]) return 'pool';
14625    if (parts[0] === 'cargar-lote-rosgan') return 'rosganLotLoad';
14626    if (parts[0] === 'remates' && parts[1] === 'crear') return 'rosganRemateCreate';
14627    if (parts[0] === 'remates' && parts[1] && parts[2] === 'auditor') return 'rosganAuditor';
14628    if (parts[0] === 'remates' && parts[1] && parts[2] === 'resultados') return 'rosganResults';
14629    if (parts[0] === 'remates' && parts[1] && parts[2] === 'estadisticas') return 'rosganStats';
14630    if (parts[0] === 'remates' && parts[1] && parts[2] === 'comercial') return 'rosganCommercial';
14631    if (parts[0] === 'remates' && parts[1] && parts[2] === 'precios') return 'rosganPriceBoard';
14632    if (parts[0] === 'remates' && parts[1] && parts[2] === 'tv' && parts[3] === 'config') return 'rosganTvConfig';
14633    if (parts[0] === 'remates' && parts[1] && parts[2] === 'tv') return 'rosganTvLower';
14634    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'auditor') return 'cabanaAuditor';
14635    if (parts[0] === 'cabanas' && parts[1] === 'crear') return 'remateConfig';
14636    if (parts[0] === 'cabanas' && parts[1] === 'estadisticas' && !parts[2]) return 'cabanaStats';
14637    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'estadisticas') return 'cabanaStats';
14638    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'comercial') return 'cabanaCommercial';
14639    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'precios') return 'cabanaPriceBoard';
14640    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'tv' && parts[3] === 'config') return 'cabanaTvConfig';
14641    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'tv') return 'cabanaTvLower';
14642    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'preofertas') return 'cabanaPrebidPanel';
14643    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'orden-ventas') return 'cabanaSalesOrder';
14644    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'editar') return 'remateConfig';
14645    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'crear-lote') return 'cabanaLotCreate';
14646    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'lotes' && parts[3] && parts[4] === 'editar') return 'cabanaLotCreate';
14647    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'lotes' && parts[3]) return 'cabanaLotDetail';
14648    if (parts[0] === 'cabanas' && parts[1]) return 'cabanaDetail';
14649    if (parts[0] === 'dev' && parts[1] === 'actividad') return 'devActivity';
14650    if (parts[0] === 'perfil' && parts[1] === 'mailing') return 'mailingAdmin';
14651    const routes = {
14652      inicio: 'home', home: 'home',
14653      roslot: 'pool', pool: 'pool',
14654      remate: 'remates', remates: 'remates',
14655      cabana: 'cabanas', cabanas: 'cabanas',
14656      feria: 'feria', ferias: 'feria',
14657      mercado: 'mercado', mercados: 'mercado', estadisticas: 'mercado',
14658      perfil: 'profile', profile: 'profile',
14659      mailing: 'mailingAdmin', 'mailing-admin': 'mailingAdmin',
14660      notificaciones: 'notifications', notifications: 'notifications',
14661      'cargar-lote': 'load', load: 'load', 'cargar-lote-rosgan': 'rosganLotLoad',
14662    };
14663    return routes[parts[0]] || '';
14664  }
14665  isPublicCabanaPath(pathname = window.location.pathname) {
14666    let decoded = String(pathname || '/');
14667    try { decoded = decodeURIComponent(decoded); } catch {}
14668    const path = decoded.replace(/\/+$/, '') || '/';
14669    const normalized = path.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
14670    if (normalized === '/cabanas' || normalized === '/cabanas/estadisticas') return true;
14671    const target = this.cabanaPathTarget(path);
14672    return !!target && ['detail', 'lot', 'stats'].includes(target.action);
14673  }
14674  isPublicRematePath(pathname = window.location.pathname) {
14675    let decoded = String(pathname || '/');
14676    try { decoded = decodeURIComponent(decoded); } catch {}
14677    const path = decoded.replace(/\/+$/, '') || '/';
14678    const normalized = path.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
14679    if (normalized === '/remates') return true;
14680    const target = this.rematePathTarget(path);
14681    return !!target && ['detail', 'lot', 'tv', 'results'].includes(target.action);
14682  }
14683  isPublicCatalogPath(pathname = window.location.pathname) {
14684    return this.isPublicRematePath(pathname) || this.isPublicCabanaPath(pathname);
14685  }
14686  activatePublicCatalogGuest(pathname = window.location.pathname) {
14687    if (this.state.user || window.firebase?.auth?.().currentUser) return false;
14688    if (!this.isPublicCatalogPath(pathname)) return false;
14689    const isRemates = this.isPublicRematePath(pathname);
14690    const route = this.routeFromPath(pathname) || (isRemates ?'remates' : 'cabanas');
14691    this.setState({
14692      user: null,
14693      apiUser: null,
14694      userConsignataria: null, userConsignatarias: [],
14695      publicAdvanceMode: '',
14696      route,
14697      routeLoading: !!(this.rematePathTarget(pathname) || this.cabanaPathTarget(pathname)),
14698      authReady: true,
14699      authBusy: false,
14700      authMessage: '',
14701      emailVerificationRequired: false,
14702      phoneVerificationRequired: false,
14703      notificationsItems: null,
14704      notificationsOpen: false,
14705    }, () => {
14706      if (isRemates) {
14707        if (this.state.monthRemates === null) this.fetchMonthRemates();
14708        this.fetchLatestFinishedRemates();
14709        if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas();
14710        this.loadRosganRemateAdmin();
14711        if (this.rematePathTarget(pathname)) this.navigateFromPath(pathname);
14712      } else if (route === 'cabanas') this.fetchCabanas();
14713      else this.navigateFromPath(pathname);
14714    });
14715    return true;
14716  }
14717  remateListPlace(item) {
14718    const city = [item.localidad || item.location?.localidad, item.provincia || item.location?.provinciaNombre].filter(Boolean).join(', ');
14719    const venue = String(item.lugar || item.location?.lugar || item.ubicacion_remate || '').trim();
14720    const location = city || venue || 'Ubicación a confirmar';
14721    return { location, venue: city && this.consKey(venue) !== this.consKey(city) ? venue : '', hasVenue: !!city && !!venue && this.consKey(venue) !== this.consKey(city) };
14722  }
14723  rematePathTarget(pathname = window.location.pathname) {
14724    let decoded = String(pathname || '');
14725    try { decoded = decodeURIComponent(decoded); } catch {}
14726    const parts = decoded.replace(/\/+$/, '').split('/').filter(Boolean);
14727    if (String(parts[0] || '').toLowerCase() !== 'remates' || !parts[1] || String(parts[1]).toLowerCase() === 'crear') return null;
14728    const third = String(parts[2] || '');
14729    const fourth = String(parts[3] || '');
14730    const section = third.toLowerCase();
14731    const action = section === 'auditor' ?'auditor'
14732      : section === 'orden-ventas' ?'salesOrder'
14733      : section === 'comercial' ?'commercial'
14734      : section === 'resultados' ?'results'
14735      : section === 'estadisticas' ?'stats'
14736      : section === 'precios' ?'prices'
14737      : section === 'tv' && fourth.toLowerCase() === 'config' ?'tvConfig'
14738      : section === 'tv' ?'tv'
14739      : third ?'lot' : 'detail';
14740    return { remateId: String(parts[1]), lotId: action === 'lot' ?third : '', action, salesOrder: action === 'salesOrder' };
14741  }
14742  roslotLotPathTarget(pathname = window.location.pathname) {
14743    let decoded = String(pathname || '');
14744    try { decoded = decodeURIComponent(decoded); } catch {}
14745    const parts = decoded.replace(/\/+$/, '').split('/').filter(Boolean);
14746    if (String(parts[0] || '').toLowerCase() !== 'roslot' || String(parts[1] || '').toLowerCase() !== 'lotes' || !parts[2]) return null;
14747    return { cert: String(parts.slice(2).join('/')).trim() };
14748  }
14749  cabanaPathTarget(pathname = window.location.pathname) {
14750    let decoded = String(pathname || '');
14751    try { decoded = decodeURIComponent(decoded); } catch {}
14752    const rawParts = decoded.replace(/\/+$/, '').split('/').filter(Boolean);
14753    const parts = rawParts.map((part) => String(part).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''));
14754    if (parts[0] !== 'cabanas') return null;
14755    if (parts[1] === 'crear') return { action: 'create', auctionId: '', lotCert: '' };
14756    if (parts[1] === 'estadisticas' && !parts[2]) return { action: 'statsIndex', auctionId: '', lotCert: '' };
14757    if (!parts[1]) return null;
14758    const sectionActions = {
14759      auditor: 'auditor', estadisticas: 'stats', comercial: 'commercial', precios: 'prices',
14760      preofertas: 'prebids', 'orden-ventas': 'salesOrder', editar: 'edit', 'crear-lote': 'createLot',
14761    };
14762    const action = parts[2] === 'tv' ?(parts[3] === 'config' ?'tvConfig' : 'tv')
14763      : (parts[2] === 'lotes' && parts[3] ?(parts[4] === 'editar' ?'editLot' : 'lot')
14764      : (sectionActions[parts[2]] || 'detail'));
14765    return { action, auctionId: String(rawParts[1] || ''), lotCert: ['lot', 'editLot'].includes(action) ?String(rawParts[3] || '') : '' };
14766  }
14767  legacyCabanaHashPath(hash = window.location.hash) {
14768    const raw = String(hash || '').replace(/^#\/?/, '').split('?')[0];
14769    let decoded = raw;
14770    try { decoded = decodeURIComponent(raw); } catch {}
14771    const parts = decoded.split('/').filter(Boolean);
14772    const root = String(parts[0] || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
14773    if (root === 'cabanas') {
14774      if (!parts[1]) return '/cabanas';
14775      const base = '/cabanas/' + encodeURIComponent(parts[1]);
14776      if (String(parts[2] || '').toLowerCase() === 'lote' && parts[3]) return base + '/lotes/' + encodeURIComponent(parts.slice(3).join('/'));
14777      if (String(parts[2] || '').toLowerCase() === 'orden-ventas') return base + '/orden-ventas';
14778      return base;
14779    }
14780    if (root === 'crear-remate') return parts[1] ?'/cabanas/' + encodeURIComponent(parts.slice(1).join('/')) + '/editar' : '/cabanas/crear';
14781    if (root === 'crear-lote-cabana' && parts[1]) return '/cabanas/' + encodeURIComponent(parts.slice(1).join('/')) + '/crear-lote';
14782    return '';
14783  }
14784  legacyRoslotHashPath(hash = window.location.hash) {
14785    const raw = String(hash || '').replace(/^#\/?/, '').split('?')[0];
14786    let decoded = raw;
14787    try { decoded = decodeURIComponent(raw); } catch {}
14788    const parts = decoded.split('/').filter(Boolean);
14789    if (String(parts[0] || '').toLowerCase() !== 'lot' || !parts[1]) return '';
14790    return this.roslotLotPath(parts.slice(1).join('/'));
14791  }
14792  roslotLotPath(cert) {
14793    const value = String(cert || '').trim();
14794    return value ?'/roslot/lotes/' + encodeURIComponent(value) : '/roslot';
14795  }
14796  syncRoslotLotLocation(cert, { replace = false } = {}) {
14797    if (this.state.publicAdvanceMode) return;
14798    const path = this.roslotLotPath(cert);
14799    let currentPath = window.location.pathname;
14800    try { currentPath = decodeURIComponent(currentPath); } catch {}
14801    let targetPath = path;
14802    try { targetPath = decodeURIComponent(path); } catch {}
14803    if (currentPath.replace(/\/+$/, '') === targetPath.replace(/\/+$/, '') && !window.location.search && !window.location.hash) return;
14804    try { this.writeAppHistory(path, { replace }); } catch {}
14805  }
14806  cabanaPath(item = this.state.currentCabana, suffix = '') {
14807    const auctionId = this.cabanaRemateId(item);
14808    if (!auctionId) return '/cabanas';
14809    return '/cabanas/' + encodeURIComponent(auctionId) + (suffix ?'/' + suffix : '');
14810  }
14811  syncCabanaLocation(item = this.state.currentCabana, suffix = '', { replace = false } = {}) {
14812    if (this.state.publicAdvanceMode === 'lacassina') return;
14813    const path = this.cabanaPath(item, suffix);
14814    const normalizedSuffix = String(suffix || '').replace(/^\/+|\/+$/g, '').toLowerCase();
14815    const publicRoute = normalizedSuffix === 'tv' ?'cabana-tv' : (normalizedSuffix === 'tv/config' ?'cabana-tv-config' : 'cabanas-public');
14816    window.__ROSGAN_PUBLIC_PATH__ = path;
14817    window.__ROSGAN_PUBLIC_ROUTE__ = publicRoute;
14818    document.documentElement.dataset.publicRoute = publicRoute;
14819    let currentPath = window.location.pathname;
14820    try { currentPath = decodeURIComponent(currentPath); } catch {}
14821    let targetPath = path;
14822    try { targetPath = decodeURIComponent(path); } catch {}
14823    if (currentPath.replace(/\/+$/, '') === targetPath.replace(/\/+$/, '') && !window.location.search && !window.location.hash) return;
14824    try { this.writeAppHistory(path, { replace }); } catch {}
14825  }
14826  remateRouteId(item) {
14827    return String(item?.id_remate || item?.sourceId || item?.id || '').trim();
14828  }
14829  remateLotRouteId(item) {
14830    return String(item?.id_certificado || item?.nro_certificado || item?.lotCert || item?.id || item?.orden_final || item?.orden_venta || '').trim();
14831  }
14832  rematePath(item, lot = null) {
14833    const remateId = this.remateRouteId(item);
14834    if (!remateId) return '/remates';
14835    const lotId = lot ?this.remateLotRouteId(lot) : '';
14836    return '/remates/' + encodeURIComponent(remateId) + (lotId ?'/' + encodeURIComponent(lotId) : '');
14837  }
14838  remateSectionPath(item, suffix = '') {
14839    const remateId = this.remateRouteId(item);
14840    return remateId ?'/remates/' + encodeURIComponent(remateId) + (suffix ?'/' + suffix : '') : '/remates';
14841  }
14842  syncRemateSectionLocation(item, suffix = '', { replace = false } = {}) {
14843    if (this.state.publicAdvanceMode) return;
14844    try { this.writeAppHistory(this.remateSectionPath(item, suffix), { replace }); } catch {}
14845  }
14846  syncRemateLocation(item, lot = null, { replace = false } = {}) {
14847    if (this.state.publicAdvanceMode) return;
14848    const path = this.rematePath(item, lot);
14849    try { this.writeAppHistory(path, { replace }); } catch {}
14850  }
14851  syncAppLocation(route, { replace = false } = {}) {
14852    if (this.state.publicAdvanceMode === 'lacassina') return;
14853    const path = this.appPathForRoute(route);
14854    if (!path) return;
14855    let currentPath = window.location.pathname;
14856    try { currentPath = decodeURIComponent(currentPath); } catch {}
14857    if (currentPath === path && !window.location.search && !window.location.hash) return;
14858    try { this.writeAppHistory(path, { replace }); } catch {}
14859  }
14860  writeAppHistory(path, { replace = false } = {}) {
14861    const target = new URL(path, window.location.origin);
14862    if (target.href === window.location.href) return;
14863    window.history[replace ? 'replaceState' : 'pushState']({}, '', target.pathname + target.search + target.hash);
14864  }
14865  async navigateFromPath(pathname = window.location.pathname) {
14866    const request = this._pathRequest = (this._pathRequest || 0) + 1;
14867    const deep = this.rematePathTarget(pathname) || this.cabanaPathTarget(pathname) || this.roslotLotPathTarget(pathname);
14868    if (deep) this.setState({ routeLoading: true });
14869    try { await this.resolveAppPath(pathname); }
14870    catch (error) { if (request === this._pathRequest) this.showToast(error?.message || 'No se pudo abrir la página.'); }
14871    finally { if (request === this._pathRequest) this.setState({ routeLoading: false }); }
14872  }
14873  async resolveAppPath(pathname = window.location.pathname) {
14874    if (this.state.publicAdvanceMode === 'lacassina') return;
14875    if (!this.state.user && !this.isPublicCatalogPath(pathname)) {
14876      this.setState({ route: 'login', routeLoading: false, apiUser: null, authReady: true });
14877      return;
14878    }
14879    const roslotTarget = this.roslotLotPathTarget(pathname);
14880    if (roslotTarget) {
14881      if (!this.canAccessRoslot()) {
14882        this.syncAppLocation('home', { replace: true });
14883        this.go('home', { syncPath: false });
14884        this.showToast('Por ahora Roslot está disponible solo para administradores.');
14885        return;
14886      }
14887      if (this.state.route !== 'lot' || String(this.state.selectedCert) !== roslotTarget.cert) {
14888        return this.openLot(roslotTarget.cert, false, { syncPath: false });
14889      }
14890      return;
14891    }
14892    const legacyCabanaHash = String(window.location.hash || '');
14893    if (/^#\/(?:cabanas(?:\/|$)|crear-remate\/|crear-lote-cabana\/)/i.test(legacyCabanaHash)) {
14894      this.navigateFromHash(legacyCabanaHash);
14895      return;
14896    }
14897    const cabanaTarget = this.cabanaPathTarget(pathname);
14898    if (cabanaTarget) {
14899      const requestedPrebidLot = cabanaTarget.action === 'prebids' ?String(new URLSearchParams(window.location.search || '').get('lote') || '').trim() : '';
14900      if (cabanaTarget.action === 'create') {
14901        if (this.state.route !== 'remateConfig' || this.state.remateConfigEditingId) this.openRemateConfig('Remate Cabañas', false);
14902        return;
14903      }
14904      if (cabanaTarget.action === 'statsIndex') {
14905        return this.openCabanaStatsIndex();
14906      }
14907      const requestedPath = window.location.pathname;
14908      const openTarget = (item) => {
14909        if (window.location.pathname !== requestedPath) return;
14910        if (!item) return;
14911        if (cabanaTarget.action === 'auditor') return this.openCabanaAuditor(item);
14912        else if (cabanaTarget.action === 'stats') return this.openCabanaStats(item);
14913        else if (cabanaTarget.action === 'commercial') return this.openCabanaCommercial(item);
14914        else if (cabanaTarget.action === 'prices') return this.openCabanaPriceBoard(item);
14915        else if (cabanaTarget.action === 'tv') return this.openCabanaTvLower(item);
14916        else if (cabanaTarget.action === 'tvConfig') return this.openCabanaTvConfig(item);
14917        else if (cabanaTarget.action === 'prebids') return this.openCabanaPrebidPanelPage(item, false, requestedPrebidLot);
14918        else if (cabanaTarget.action === 'salesOrder') return this.openCabanaSalesOrderPage(item, null, false);
14919        else if (cabanaTarget.action === 'edit') return this.openCabanaRemateEdit(item, false);
14920        else if (cabanaTarget.action === 'createLot') return this.openCabanaLotCreate(item, false);
14921        else if (cabanaTarget.action === 'editLot') return this.openCabanaLotEdit(item, cabanaTarget.lotCert, false);
14922        else if (cabanaTarget.action === 'lot') return this.openCabana(item, { focusLotCert: cabanaTarget.lotCert, syncPath: fal
14922se });
14923        else return this.openCabana(item, { syncPath: false });
14924      };
14925      const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === cabanaTarget.auctionId);
14926      if (item) return openTarget(item);
14927      else return this.fetchCabanaById(cabanaTarget.auctionId, { force: true }).then(openTarget);
14928    }
14929    const remateTarget = this.rematePathTarget(pathname);
14930    if (remateTarget) {
14931      if (remateTarget.action === 'results') {
14932        const source = new URLSearchParams(window.location.search).get('origen') === 'historico' ? 'historico' : 'actual';
14933        this.setState({route:'rosganResults', resultsPageKey:source+':'+remateTarget.remateId, resultsBackHref:'/remates/'+encodeURIComponent(remateTarget.remateId)});
14934        return;
14935      }
14936      if (remateTarget.action === 'auditor') {
14937        return this.openRosganAuditorById(remateTarget.remateId);
14938      }
14939      if (remateTarget.salesOrder) {
14940        const active = (this.state.rosganRemates || []).find((candidate) => this.remateRouteId(candidate) === remateTarget.remateId);
14941        if (active) return this.openRosganSalesOrderPage(active, null, false, true);
14942        else return this.openRosganSalesOrderById(remateTarget.remateId);
14943      }
14944      if (['commercial', 'prices', 'stats', 'tv', 'tvConfig'].includes(remateTarget.action)) {
14945        const active = (this.state.rosganRemates || []).find((candidate) => this.remateRouteId(candidate) === remateTarget.remateId);
14946        if (active) return this.openRosganOperations(active, remateTarget.action, { syncPath: false });
14947        else return this.openRosganOperationsById(remateTarget.remateId, remateTarget.action);
14948      }
14949      const currentRemateId = this.remateRouteId(this.state.currentRemate);
14950      if (this.state.route !== 'remateDetail' || currentRemateId !== remateTarget.remateId) {
14951        const active = (this.state.rosganRemates || []).find((candidate) => this.remateRouteId(candidate) === remateTarget.remateId);
14952        if (active) return this.openRosganActiveRemate(active, { lotId: remateTarget.lotId, syncPath: false });
14953        else return this.openRosganActiveRemateById(remateTarget.remateId, remateTarget.lotId);
14954      } else if (remateTarget.lotId) {
14955        const lot = (this.state.remateLots || []).find((candidate) => this.remateLotRouteId(candidate) === remateTarget.lotId);
14956        if (lot && this.remateLotRouteId(this.state.rdLightbox) !== remateTarget.lotId) this.openRemateLot(lot, { syncPath: false });
14957      } else if (this.state.rdLightbox) {
14958        this.setState({ rdLightbox: null });
14959      }
14960      return;
14961    }
14962    const cleanPath = String(pathname || '/').replace(/\/+$/, '') || '/';
14963    const route = this.routeFromPath(pathname) || (cleanPath === '/' ?'profile' : '');
14964    const canonicalPath = this.appPathForRoute(route);
14965    if (canonicalPath && cleanPath !== canonicalPath) this.syncAppLocation(route, { replace: true });
14966    if (route && route !== this.state.route) this.go(route, { syncPath: false });
14967  }
14968  routeFromHash() {
14969    const raw = String(window.location.hash || '').replace(/^#\/?/, '').split('?')[0].split('/')[0];
14970    const map = { home: 'home', pool: 'pool', profile: 'profile', 'mailing-admin': 'mailingAdmin', notifications: 'notifications', remates: 'remates', cabanas: 'cabanas', 'crear-remate': 'remateConfig', 'crear-lote-cabana': 'cabanaLotCreate', 'cargar-lote-rosgan': 'rosganLotLoad', feria: 'feria', mercado: 'mercado', load: 'load', 'load-lot': 'load' };
14971    return map[raw] || '';
14972  }
14973  navigateFromHash(hash = window.location.hash) {
14974    if (!this.state.user && !this.state.publicAdvanceMode) return;
14975    const path = String(hash || '').replace(/^#\/?/, '').split('?')[0];
14976    const parts = path.split('/').filter(Boolean);
14977    if (parts[0] === 'lot' && parts[1]) {
14978      if (!this.canAccessRoslot()) {
14979        this.syncAppLocation('home', { replace: true });
14980        this.go('home', { syncPath: false });
14981        this.showToast('Por ahora Roslot está disponible solo para administradores.');
14982        return;
14983      }
14984      const cert = decodeURIComponent(parts.slice(1).join('/'));
14985      const nextPath = this.roslotLotPath(cert);
14986      try { window.history.replaceState({}, '', nextPath); } catch {}
14987      if (this.state.route !== 'lot' || this.state.selectedCert !== cert) this.openLot(cert, false, { syncPath: false });
14988      return;
14989    }
14990    if (parts[0] === 'crear-lote-cabana' && parts[1]) {
14991      const id = decodeURIComponent(parts.slice(1).join('/'));
14992      const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id);
14993      if (item && (this.state.route !== 'cabanaLotCreate' || this.cabanaRemateId(this.state.cabanaLotCreateRemate) !== id)) this.openCabanaLotCreate(item);
14994      else if (!this.state.cabanas && !this.state.cabanasLoading) this.fetchCabanas().then(() => this.navigateFromHash(hash));
14995      else if (!this.state.cabanasLoading) this.fetchCabanaById(id).then((remate) => remate && this.openCabanaLotCreate(remate)).catch(() => {});
14996      return;
14997    }
14998    if (parts[0] === 'crear-remate' && parts[1]) {
14999      const id = decodeURIComponent(parts.slice(1).join('/'));
15000      const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id);
15001      if (item && (this.state.route !== 'remateConfig' || this.state.remateConfigEditingId !== id)) this.openCabanaRemateEdit(item);
15002      else if (!this.state.cabanas && !this.state.cabanasLoading) this.fetchCabanas().then(() => this.navigateFromHash(hash));
15003      if (!this.state.cabanasLoading) this.fetchCabanaById(id, { force: true }).then((remate) => remate && this.openCabanaRemateEdit(remate)).catch(() => {});
15004      return;
15005    }
15006    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'orden-ventas') {
15007      const id = decodeURIComponent(parts[1]);
15008      const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id);
15009      if (item) {
15010        if (this.state.route !== 'cabanaSalesOrder' || this.cabanaRemateId(this.state.currentCabana) !== id) {
15011          this.openCabanaSalesOrderPage(item);
15012        }
15013      } else if (!this.state.cabanasLoading) {
15014        this.fetchCabanaById(id, { force: true })
15015          .then((remate) => remate && this.openCabanaSalesOrderPage(remate))
15016          .catch(() => {});
15017      }
15018      return;
15019    }
15020    if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'lote' && parts[3]) {
15021      const id = decodeURIComponent(parts[1]);
15022      const lotCert = decodeURIComponent(parts.slice(3).join('/'));
15023      const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id);
15024      if (item) this.openCabana(item, { focusLotCert: lotCert });
15025      else if (!this.state.cabanasLoading) {
15026        this.fetchCabanaById(id, { force: true })
15027          .then((remate) => remate && this.openCabana(remate, { focusLotCert: lotCert }))
15028          .catch(() => {});
15029      }
15030      return;
15031    }
15032    if (parts[0] === 'cabanas' && parts[1]) {
15033      const id = decodeURIComponent(parts.slice(1).join('/'));
15034      const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id);
15035      if (item && (this.state.route !== 'cabanaDetail' || this.cabanaRemateId(this.state.currentCabana) !== id)) this.openCabana(item);
15036      else if (!this.state.cabanas && !this.state.cabanasLoading) this.fetchCabanas().then(() => this.navigateFromHash(hash));
15037      if (!this.state.cabanasLoading) this.fetchCabanaById(id, { force: true }).then((remate) => remate && this.openCabana(remate)).catch(() => {});
15038      return;
15039    }
15040    const route = this.routeFromHash();
15041    if (route && route !== this.state.route) this.go(route);
15042  }
15043  restoreRoute() {
15044    const allowed = ['home', 'pool', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'rosganSalesOrder', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'cabanaTvLower', 'cabanaTvConfig', 'remateConfig', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaSalesOrder', 'channel', 'mercado'];
15045    const route = this.routeFromHash() || this.routeFromPath() || sessionStorage.getItem('rosgan:lastRoute') || 'home';
15046    return allowed.includes(route) ?route : 'home';
15047  }
15048  rememberRoute(route) {
15049    const allowed = ['home', 'pool', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'rosganSalesOrder', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'cabanaTvLower', 'cabanaTvConfig', 'remateConfig', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaSalesOrder', 'channel', 'mercado'];
15050    if (!allowed.includes(route)) return;
15051    try { sessionStorage.setItem('rosgan:lastRoute', route); } catch {}
15052  }
15053  // ---- Comitentes y establecimientos ----
15054  editComitenteAlias(id) {
15055    if (!this.canManageComitentes(this.state.apiUser)) return;
15056    const item = [...(this.state.rosganLotComitentes || []), ...(this.state.comitentes || [])].find(c =>
15056 String(c.id) === String(id));
15057    window.RosganComitenteAlias.open({ id, scoped: !!item?.consignatariaId, apiFetch: this.apiFetch.bind(this), onSaved: async () => {
15058      await this.fetchComitentes();
15059      if (this.rosganForm().consignatariaId) {
15060        await this.loadRosganLotRelations();
15061        const selected = (this.state.rosganLotComitentes || []).find(c => String(c.id) === String(this.rosganForm().comitenteId));
15062        if (selected) this.setState({ rosganComitenteSearch: selected.alias || selected.name || selected.fiscalName || '' });
15063      }
15064    }});
15065  }
15066  emptyComitenteForm() { return { alias: '', cuitCuil: '', nombreFiscal: '', provincia: '', localidad: '', source: '' }; }
15067  normComitente(item) {
15068    if (!item) return null;
15069    return {
15070      id: String(item.id || item.comitenteId || item._id || ''),
15071      consignatariaId: String(item.consignatariaId || ''),
15072      cuitCuil: String(item.cuitCuil || item.cuitNormalized || item.cuit || ''),
15073      rawAlias: item.alias || '',
15074      alias: item.alias || item.name || item.nombre || item.nombreFiscal || item.fiscalName || 'Comitente',
15075      nombreFiscal: item.nombreFiscal || item.fiscalName || item.razonSocial || '',
15076      provincia: item.provincia || item.province || '', localidad: item.localidad || item.locality || '',
15077    };
15078  }
15079  profileComitentesScope() {
15080    const uid = this.state.user?.uid;
15081    if (!uid) return '';
15082    const ids = [...new Set((this.state.userConsignatarias || []).map((item) => String(item.id || '')).filter(Boolean))].sort();
15083    return this.isRosganProfile() ? (ids.length ? uid + ':' + ids.join('|') : '') : uid + ':mine';
15084  }
15085  visibleProfileComitentes() {
15086    if (this.state.comitentesScope !== this.profileComitentesScope() || !this.profileComitentesScope()) return [];
15087    const assigned = new Set((this.state.userConsignatarias || []).map((item) => String(item.id)));
15088    return (this.state.comitentes || []).filter((item) => !this.isRosganProfile() || assigned.has(String(item.consignatariaId)));
15089  }
15090  profileComitenteDirectory() {
15091    const all = this.visibleProfileComitentes();
15092    const normalize = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
15093    const query = normalize(this.state.profileComitenteQuery);
15094    const province = this.state.profileComitenteProvince || '';
15095    const locality = this.state.profileComitenteLocality || '';
15096    const options = values => [...new Set(values.filter(Boolean))].sort((a,b) => a.localeCompare(b,'es',{sensitivity:'base'}));
15097    const filtered = all.filter(item => (!province || item.provincia === province) && (!locality || item.localidad === locality) &&
15098      (!query || normalize([item.alias, item.nombreFiscal, item.cuitCuil].join(' ')).includes(query) ||
15099        (/^[\d\s-]+$/.test(query) && String(item.cuitCuil || '').replace(/\D/g,'').includes(query.replace(/\D/g,'')))))
15100      .sort((a,b) => (a.alias || a.nombreFiscal || '').localeCompare(b.alias || b.nombreFiscal || '', 'es', {sensitivity:'base'}));
15101    const pages = Math.max(1, Math.ceil(filtered.length / 25));
15102    const page = Math.max(1, Math.min(pages, this.state.profileComitentePage || 1));
15103    return { total: all.length, count: filtered.length, page, pages, rows: filtered.slice((page-1)*25,page*25),
15104      provinces: options(all.map(item => item.provincia)),
15105      localities: options(all.filter(item => !province || item.provincia === province).map(item => item.localidad)) };
15106  }
15107  async fetchComitentes() {
15108    const scope = this.profileComitentesScope();
15109    const request = this._profileComitentesRequest = (this._profileComitentesRequest || 0) + 1;
15110    this.setState({ comitentes: [], comitentesScope: '', comitentesLoading: !!scope, comitentesError: '' });
15111    if (!scope) return;
15112    const current = () => request === this._profileComitentesRequest && scope === this.profileComitentesScope();
15113    try {
15114      let items;
15115      if (this.isRosganProfile()) {
15116        const ids = [...new Set((this.state.userConsignatarias || []).map((item) => String(item.id || '')).filter(Boolean))];
15117        const results = await Promise.all(ids.map(async (id) => {
15118          const data = await this.apiFetch('/api/Comitentes?consignatariaId=' 
15118+ encodeURIComponent(id));
15119          return this.listFrom(data, ['items', 'data', 'comitentes']).filter((item) => String(item.consignatariaId || '') === id);
15120        }));
15121        items = results.flat();
15122      } else {
15123        items = this.listFrom(await this.apiFetch('/api/Comitentes/mine'), ['items', 'data', 'comitentes']);
15124      }
15125      if (!current()) return;
15126      const unique = new Map(items.map((item) => this.normComitente(item)).filter((item) => item?.id).map((item) => [item.consignatariaId + ':' + item.id, item]));
15127      this.setState({ comitentes: [...unique.values()], comitentesScope: scope, comitentesLoading: false });
15128    } catch (error) {
15129      if (!current()) return;
15130      this.setState({ comitentes: [], comitentesScope: '', comitentesLoading: false, comitentesError: error?.message || 'No se pudieron cargar tus comitentes.' });
15131    }
15132  }
15133  setComitenteForm(key, value) {
15134    const next = { ...(this.state.comitenteForm || this.emptyComitenteForm()), [key]: value };
15135    if (key === 'cuitCuil') { next.cuitCuil = String(value || '').replace(/\D/g, '').slice(0, 11); next.source = ''; }
15136    this.setState({ comitenteForm: next, comitenteFormMessage: null });
15137  }
15138  onComitenteCuit(value) {
15139    const cuitCuil = String(value || '').replace(/\D/g, '').slice(0, 11);
15140    if (cuitCuil.length < 11) {
15141      this._comitenteLookupToken = (this._comitenteLookupToken || 0) + 1;
15142      this._comitenteLookupKey = '';
15143    }
15144    this.setState({
15145      comitenteForm: { ...(this.state.comitenteForm || this.emptyComitenteForm()), cuitCuil, nombreFiscal: '', provincia: '', localidad: '', source: '' },
15146      comitenteLookupLoading: false, comitenteFormMessage: null,
15147    }, () => { if (cuitCuil.length === 11) this.lookupComitenteCuit(cuitCuil); });
15148  }
15149  async lookupComitenteCuit(cuit) {
15150    const normalized = String(cuit || '').replace(/\D/g, '');
15151    if (normalized.length !== 11) return;
15152    if (this._comitenteLookupKey === normalized && this.state.comitenteLookupLoading) return;
15153    this._comitenteLookupKey = normalized;
15154    const token = (this._comitenteLookupToken || 0) + 1;
15155    this._comitenteLookupToken = token;
15156    this.setState({ comitenteLookupLoading: true, comitenteFormMessage: { ok: true, text: 'Consultando datos fiscales en ARCA…' } });
15157    try {
15158      const response = await this.apiFetch(this.roslotCuitLookupEndpoint(normalized));
15159      if (token !== this._comitenteLookupToken) return;
15160      const data = response?.data ?? response ?? {};
15161      if (data?.error) throw new Error(String(data.error));
15162      const record = data?.comitente || data?.item || data;
15163      const existingId = String(record?.id || record?.comitenteId || '');
15164      const source = String(data?.source || record?.source || '').toLowerCase();
15165      const localItem = (this.state.comitentes || []).find((item) => String(item.id) === existingId || String(item.cuitCuil || '').replace(/\D/g, '') === normalized);
15166      if ((source === 'local' || localItem) && (existingId || localItem?.id)) {
15167        const selectedId = String(existingId || localItem.id);
15168        this.setState({
15169          comitenteLookupLoading: false, comitenteFormOpen: false, comitenteForm: this.emptyComitenteForm(),
15170          estForm: { ...(this.state.estForm || this.emptyEstForm()), comitenteId: selectedId },
15171          comitenteFormMessage: null, estFormMessage: { ok: true, text: 'El comitente ya existía y quedó seleccionado.' },
15172        });
15173        return;
15174      }
15175      const found = data?.found === true || (data?.found !== false && Boolean(record?.nombreFiscal || record?.fiscalName || record?.razonSocial || record?.name || record?.nombre));
15176      if (!found) throw new Error('ARCA no encontró este CUIT. Revisá los 11 dígitos.');
15177      const nombreFiscal = String(record?.nombreFiscal || record?.fiscalName || record?.razonSocial || record?.name || record?.nombre || '').trim();
15178      const provincia = String(record?.provincia || record?.province || '').trim();
15179      const localidad = String(record?.localidad || record?.locality || '').trim();
15180      this.setState({
15181        comitenteLookupLoading: false,
15182        comitenteForm: { alias: this.state.comitenteForm?.alias || '', cuitCuil: normalized, nombreFiscal, provincia, localidad, source: source || 'arca' },
15183        comitenteFormMessage: { ok: true, text: 'Datos fiscales obtenidos de ARCA.' },
15184      });
15185    } catch (error) {
15186      if (token !== this._comitenteLookupToken) return;
15187      this.setState({ comitenteLookupLoading: false, comitenteFormMessage: { ok: false, text: error?.message || 'No se pudo consultar ARCA.' } });
15188    }
15189  }
15190  async saveComitente() {
15191    if (this.state.comitenteSaving) return;
15192    const form = this.state.comitenteForm || this.emptyComitenteForm();
15193    if (!String(form.alias || '').trim()) { this.setState({comitenteFormMessage:{ok:false,text:'Ingresá el al
15193ias del comitente.'}}); return; }
15194    this.setState({ comitenteSaving: true, comitenteFormMessage: null });
15195    try {
15196      const cuit = String(form.cuitCuil || '').replace(/\D/g, '');
15197      const razonSocial = String(form.nombreFiscal || '').trim();
15198      const provincia = String(form.provincia || '').trim();
15199      const localidad = String(form.localidad || '').trim();
15200      const created = await this.apiFetch(form.source && cuit ? '/api/Comitentes' : '/api/legacy-comitentes', { method: 'POST', body: JSON.stringify({
15201        ...(!form.source || !cuit ? {action:'create_alias'} : {}),
15202        cuitCuil: cuit,
15203        alias: String(form.alias || '').trim(),
15204        nombreFiscal: razonSocial,
15205        provincia,
15206        localidad,
15207      }) });
15208      const createdId = String(created?.comitente?.id || created?.id || created?.data?.comitente?.id || created?.data?.id || '');
15209      this.setState({ comitentes: null, comitenteSaving: false, comitenteFormOpen: false, comitenteForm: this.emptyComitenteForm(), comitenteFormMessage: null,
15210        estForm: createdId ?{ ...(this.state.estForm || this.emptyEstForm()), comitenteId: createdId } : this.state.estForm });
15211      await this.fetchComitentes();
15212      if (!createdId) {
15213        const match = (this.state.comitentes || []).find((item) => String(item.cuitCuil || '').replace(/\D/g, '') === String(form.cuitCuil));
15214        if (match?.id) this.setState({ estForm: { ...(this.state.estForm || this.emptyEstForm()), comitenteId: match.id } });
15215      }
15216      this.showToast('Comitente creado correctamente.');
15217    } catch (error) {
15218      const message = String(error?.message || 'No se pudo crear el comitente.').replace(/CUIT\s*\/\s*CUIL/gi, 'CUIT').replace(/\s+para esta consignataria\.?/gi, '.');
15219      if (/ya existe un comitente con ese CUIT/i.test(message)) {
15220        await this.fetchComitentes();
15221        const match = (this.state.comitentes || []).find((item) => String(item.cuitCuil || '').replace(/\D/g, '') === String(form.cuitCuil || '').replace(/\D/g, ''));
15222        if (match?.id) {
15223          this.setState({
15224            comitenteSaving: false, comitenteFormOpen: false, comitenteForm: this.emptyComitenteForm(), comitenteFormMessage: null,
15225            estForm: { ...(this.state.estForm || this.emptyEstForm()), comitenteId: match.id },
15226          });
15227          this.showToast('El comitente ya existía y quedó seleccionado.');
15228          return;
15229        }
15230      }
15231      this.setState({ comitenteSaving: false, comitenteFormMessage: { ok: false, text: message } });
15232    }
15233  }
15234  emptyEstForm() { return { nombre: '', provincia: '', localidad: '', comitenteId: '', renspa: '', latitud: '', longitud: '' }; }
15235  normEst(e) {
15236    if (!e) return null;
15237    return {
15238      id: String(e.id || e.establishmentId || e.establecimientoId  || e._id || ''),
15239      nombre: e.nombre || e.name  || 'Establecimiento',
15240      provincia: e.provincia  || e.province || '',
15241      localidad: e.localidad  || e.locality || '',
15242      comitenteId: String(e.comitenteId || e.comitente?.id || ''),
15243      comitenteAlias: e.comitenteAlias || e.comitente?.alias || e.comitente?.name || '',
15244      comitenteCuitCuil: e.comitenteCuitCuil || e.comitente?.cuitCuil || e.comitente?.cuit || '',
15245      comitenteNombreFiscal: e.comitenteNombreFiscal || e.comitente?.nombreFiscal || e.comitente?.fiscalName || '',
15246      comitente: e.comitenteAlias || e.comitenteNombreFiscal || e.comitente?.alias || e.comitente?.nombreFiscal || e.comitente || e.principal || '',
15247      renspa: e.renspa || null,
15248      latitud: e.latitud || e.latitude  || e.lat || null,
15249      longitud: e.longitud || e.longitude || e.lng  || e.lon || null,
15250    };
15251  }
15252  async fetchEstablishments() {
15253    if (this.state.establishmentsLoading) return;
15254    const request = this._profileEstablishmentsRequest = (this._profileEstablishmentsRequest || 0) + 1;
15255    this.setState({ establishmentsLoading: true, establishmentsError: '' });
15256    try {
15257      const certifierView = this.isCertifierOnly(this.state.apiUser);
15258      const data = await this.apiFetch(certifierView ?'/api/rosgan-establishments' : '/api/Establishments/mine?limit=50');
15259      if (request !== this._profileEstablishmentsRequest) return;
15260      const assigned = new Set((this.state.userConsignatarias || []).map((item) => String(item.id)));
15261      const arr = this.listFrom(data, ['items', 'data', 'establishments']).filter((item) => !certifierView || assigned.has(String(item.consignatariaId)));
15262      this.setProfileFetchInfo('establecimientos', arr.length, '');
15263      this.setState({ establishments: arr.map((e) => this.normEst(e)).filter((e) => e && e.id), establishmentsLoading: false, establishmentsError: '' });
15264    } catch (e) { if (request !== this._profileEstablishmentsRequest) return; this.setProfileFetchInfo('establecimientos', null, e?.message || 'Error'); this.setState({ establishments: [], establishmentsLoading: false, establishmentsError: e?.message || 'No se pudieron cargar tus establecimientos.' }); }
15265  }
15266  async createEstablishment(data) {
15267    const body = {
15268      nombre: data.nombre || '', provincia: data.provincia || '', localidad: data.localidad || '',
15269      comitenteId: data.comitenteId || '', renspa: String(data.renspa || '').trim() || null,
15270      latitud: this.numOrNull(data.latitud), longitud: this.numOrNull(data.longitud),
15271    };
15272    const res = await this.apiFetch('/api/Establishments', { method: 'POST', body: JSON.stringify(body) });
15273    const created = res?.establishment || res?.establecimiento || res?.data?.establishment || res?.data?.establecimiento || res?.data || res || {};
15274    return this.normEst({ ...body, ...created });
15275  }
15276  async saveEstablishment(forLot) {
15277    if (this.state.estSaving) return;
15278    if (!this.canManageComitentes(this.state.apiUser)) {
15279      this.setState({ estFormOpen: false, estFormMessage: { text: 'No tenés permisos para gestionar comitentes.', ok: false } }); return;
15280    }
15281    const ef = this.state.estForm || this.emptyEstForm();
15282    if (!String(ef.comitenteId || '').trim() || !String(ef.nombre).trim() || !String(ef.provincia).trim() || !String(ef.localidad).trim()) {
15283      this.setState({ estFormMessage: { text: 'Seleccioná un comitente y completá n
15283ombre, provincia y localidad.', ok: false } }); return;
15284    }
15285    this.setState({ estSaving: true, estFormMessage: { text: 'Guardando establecimiento…', ok: true } });
15286    try {
15287      const created = await this.createEstablishment(ef);
15288      await this.fetchEstablishments();
15289      const saved = (this.state.establishments || []).find((item) => item.id === created?.id) || created;
15290      const patch = { estSaving: false, estFormOpen: false, estForm: this.emptyEstForm(), estFormMessage: null };
15291      if (forLot && saved && saved.id) {
15292        patch.form = { ...this.form(), estId: saved.id, estName: saved.nombre, comitente: saved.comitente || '', prov: saved.provincia, loc: saved.localidad, lat: saved.latitud != null ?String(saved.latitud) : '', lon: saved.longitud != null ?String(saved.longitud) : '' };
15293      }
15294      this.setState(patch);
15295      this.showToast('Establecimiento "' + (created?.nombre || ef.nombre) + '" guardado.');
15296    } catch {
15297      this.setState({ estSaving: false, estFormMessage: { text: 'No se pudo guardar. Verificá tu sesión.', ok: false } });
15298    }
15299  }
15300  async deleteEstablishment(id) {
15301    if (!id || this.state.estDeleting) return;
15302    this.setState({ estDeleting: id });
15303    try {
15304      await this.apiFetch('/api/Establishments/' + encodeURIComponent(id), { method: 'DELETE' });
15305      this.setState({ establishments: (this.state.establishments || []).filter((e) => e.id !== id), estDeleting: '' });
15306      this.showToast('Establecimiento eliminado.');
15307    } catch { this.setState({ estDeleting: '' }); this.showToast('No se pudo eliminar.'); }
15308  }
15309  setEstForm(key, val) {
15310    const ef = { ...(this.state.estForm || this.emptyEstForm()), [key]: val };
15311    if (key === 'provincia') { ef.localidad = ''; ef.latitud = ''; ef.longitud = ''; }
15312    this.setState({ estForm: ef, estFormMessage: null, ...(key === 'provincia' ?{ estPlaceResults: [], estPlaceError: '' } : {}) });
15313  }
15314  onEstPlaceInput(value) {
15315    const localidad = String(value || '');
15316    const form = { ...(this.state.estForm || this.emptyEstForm()), localidad, latitud: '', longitud: '' };
15317    this.setState({ estForm: form, estFormMessage: null, estPlaceError: '', estPlaceResults: localidad.trim().length >= 3 ?this.state.estPlaceResults : [] });
15318    clearTimeout(this._estPlaceTimer);
15319    if (!form.provincia || localidad.trim().length < 3) return;
15320    this._estPlaceTimer = setTimeout(() => this.searchEstPlaces(localidad, form.provincia), 280);
15321  }
15322  async searchEstPlaces(localidad, provincia) {
15323    const query = String(localidad || '').trim();
15324    if (query.length < 3) return;
15325    const token = (this._estPlaceToken || 0) + 1;
15326    this._estPlaceToken = token;
15327    this.setState({ estPlaceLoading: true, estPlaceError: '' });
15328    try {
15329      const response = await this.apiFetch('/api/places/search', { method: 'POST', body: JSON.stringify({ query, provincia: String(provincia || '').trim() }) });
15330      if (token !== this._estPlaceToken) return;
15331      const results = Array.isArray(response?.data) ?response.data : this.listFrom(response, ['data', 'items', 'results']);
15332      this.setState({ estPlaceLoading: false, estPlaceResults: results.slice(0, 7), estPlaceError: results.length ?'' : 'No encontramos esa localidad. Podés escribirla manualmente.' });
15333    } catch (error) {
15334      if (token !== this._estPlaceToken) return;
15335      this.setState({ estPlaceLoading: false, estPlaceResults: [], estPlaceError: 'Google no respondió. Podés escribir la localidad manualmente.' });
15336    }
15337  }
15338  selectEstPlace(item) {
15339    if (!item) return;
15340    this.setState({
15341      estForm: { ...(this.state.estForm || this.emptyEstForm()), localidad: String(item.label || item.address || ''), latitud: item.lat != null ?String(item.lat) : '', longitud: item.lng != null ?String(item.lng) : '' },
15342      estPlaceResults: [], estPlaceError: '', estFormMessage: null,
15343    });
15344  }
15345  closeEstPlaceResults() {
15346    setTimeout(() => this.setState({ estPlaceResults: [] }), 150);
15347  }
15348  establishmentConsignatariaId() {
15349    const memberships = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(
15349Boolean);
15350    const refs = memberships.flatMap((item) => [item?.id, item?.slug, item?.ref, item?.name]).filter(Boolean);
15351    const catalogs = [...(this.state.rosganLotConsignatarias || []), ...(this.state.consignatarias || [])];
15352    const match = catalogs.find((item) => refs.some((ref) => [item?.id, item?.slug, item?.name, item?.nombre]
15353      .some((value) => value && this.consKey(value) === this.consKey(ref))));
15354    return String(match?.id || match?.consignatariaId || refs[0] || '');
15355  }
15356  async resolveEstablishmentConsignatariaId() {
15357    let id = this.establishmentConsignatariaId();
15358    if (this.state.rosganLotConsignatarias?.length) return id;
15359    try {
15360      const raw = await this.apiFetch('/api/rosgan-consignatarias');
15361      const items = this.rosganArray(raw).filter((item) => item?.isActive !== false && item?.activo !== false);
15362      await new Promise((resolve) => this.setState({ rosganLotConsignatarias: items }, resolve));
15363      id = this.establishmentConsignatariaId();
15364    } catch {}
15365    return id;
15366  }
15367  onEstCuit(value) {
15368    const cuit = String(value || '').replace(/\D/g, '').slice(0, 11);
15369    if (cuit.length < 11) {
15370      this._estLookupToken = (this._estLookupToken || 0) + 1;
15371      this._estLookupKey = '';
15372    }
15373    this.setState({
15374      estForm: { ...(this.state.estForm || this.emptyEstForm()), cuit, source: '' },
15375      estLookupLoading: false,
15376      estFormMessage: null,
15377    }, () => { if (cuit.length === 11) this.lookupEstComitente(cuit); });
15378  }
15379  async lookupEstComitente(cuit) {
15380    if (!this.canManageComitentes(this.state.apiUser)) return;
15381    const normalized = String(cuit || '').replace(/\D/g, '');
15382    if (normalized.length !== 11) return;
15383    const lookupKey = normalized;
15384    if (this._estLookupKey === lookupKey && this.state.estLookupLoading) return;
15385    this._estLookupKey = lookupKey;
15386    const token = (this._estLookupToken || 0) + 1;
15387    this._estLookupToken = token;
15388    this.setState({ estLookupLoading: true, estFormMessage: { text: 'Consultando CUIT en ARCA…', ok: true } });
15389    try {
15390      const response = await this.apiFetch(this.roslotCuitLookupEndpoint(normalized));
15391      if (token !== this._estLookupToken) return;
15392      const data = response?.data ?? response ?? {};
15393      if (data?.error) throw new Error(String(data.error));
15394      const record = data?.comitente || data?.item || data;
15395      const found = data?.found === true || (data?.found !== false && Boolean(
15396        record?.cuit || record?.cuitNormalized || record?.fiscalName || record?.razonSocial || record?.name || record?.nombre
15397      ));
15398      if (!found) {
15399        this.setState({ estLookupLoading: false, estFormMessage: { text: 'ARCA no encontró este CUIT. Revisá los 11 dígitos.', ok: false } });
15400        return;
15401      }
15402      const current = this.state.estForm || this.emptyEstForm();
15403      const provinceRaw = String(record.province || record.provincia || '').trim();
15404      const provinceOptions = this.state.geoProvinces?.length ?this.state.geoProvinces : ARGENTINA_PROVINCES;
15405      const province = provinceOptions.find((item) => this.consKey(item) === this.consKey(provinceRaw)) || provinceRaw;
15406      const locality = String(record.locality || record.localidad || '').trim();
15407      const fiscalName = String(record.fiscalName || record.razonSocial || '').trim();
15408      const visibleName = String(record.alias || record.name || record.nombre || fiscalName || current.comitente || '').trim();
15409      const verifiedSource = String(data.source || record.source || 'arca');
15410      if (province) this.fetchLocalities(province);
15411      this.setState({
15412        estLookupLoading: false,
15413        estForm: { ...current, cuit: normalized, comitente: visibleName, fiscalName, provincia: province || current.provincia, localidad: locality || current.localidad, source: verifiedSource },
15414        estFormMessage: { text: verifiedSource.toLowerCase() === 'local' ?'Comitente encontrado en los registros locales.' : 'Datos fiscales obtenidos de ARCA.', ok: true },
15415      });
15416    } catch (error) {
15417      if (token !== this._estLookupToken) return;
15418      const detail = String(error?.data?.message || error?.message || '').trim();
15419      const permission = Number(error?.status) === 403;
15420      this.setState({
15421        estLookupLoading: false,
15422        estForm: { ...(this.state.estForm || this.emptyEstForm()), source: '' },
15423        estFormMessage: { text: permission ?'El backend no autorizó a este rol para consultar ARCA.' : (detail || 'ARCA no respondió. Reintentá la consulta.'), ok: false },
15424      });
15425    }
15426  }
15427  localIsoDate() {
15428    const now = new Date();
15429    const pad = (value) => String(value).padStart(2, '0');
15430    return now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate());
15431  }
15432  emptyBaForm() { return { buyerEmail: '', buyerUid: '', creditStartDate: this.localIsoDate(), weeklyCreditAmount: '' }; }
15433  formatBaDate(s) { const d = new Date(s + 'T00:00:00'); if (isNaN(d)) return s; return d.toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' }); }
15434  formatBaCurrency(value) { const amount = Number(value); return Number.isFinite(amount) ?'$' + Math.round(amount).toLocaleStr
15434ing('es-AR') : '$0'; }
15435  calculateCreditEndDate(startDate) {
15436    const date = new Date(String(startDate || '') + 'T00:00:00');
15437    if (isNaN(date)) return '';
15438    date.setDate(date.getDate() + 6);
15439    const pad = (value) => String(value).padStart(2, '0');
15440    return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
15441  }
15442  normBuyerAuth(a) {
15443    if (!a) return null;
15444    const source = a.authorization || a.buyerAuthorization || a;
15445    const seller = source.seller || a.seller || source.owner || a.owner || source.authorizer || a.authorizer || source.grantedBy || a.grantedBy || source.createdBy || a.createdBy || source.consignataria || a.consignataria || {};
15446    const sellerFirstName = String(source.sellerFirstName || a.sellerFirstName || seller.firstName || seller.nombre || '').trim();
15447    const sellerLastName = String(source.sellerLastName || a.sellerLastName || seller.lastName || seller.apellido || '').trim();
15448    const sellerFullName = [sellerFirstName, sellerLastName].filter(Boolean).join(' ');
15449    const finite = (...values) => {
15450      const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== '' && Number.isFinite(Number(candidate)));
15451      return value === undefined ?null : Number(value);
15452    };
15453    return {
15454      id: source.id || source.authorizationId || source._id || source.docId || source.buyerUid || source.uid || '',
15455      summaryId: source.authorizationId || source.id || source._id || source.docId || '',
15456      buyerUid: source.buyerUid || source.buyerId || source.userUid || source.uid || '',
15457      buyerEmail: source.buyerEmail || source.email || source.buyerName || '',
15458      buyerName: String(source.buyerName || source.buyerDisplayName || source.displayName || source.name || source.nombre || ''),
15459      creditStartDate: String(source.creditStartDate || '').slice(0, 10),
15460      creditEndDate: String(source.creditEndDate || '').slice(0, 10),
15461      weeklyCreditAmount: finite(source.weeklyCreditAmount) || 0,
15462      sellerUid: String(source.sellerUid || a.sellerUid || source.sellerId || a.sellerId || source.consignatariaUid || a.consignatariaUid || source.ownerUid || a.ownerUid || source.authorizerUid || a.authorizerUid || source.grantedByUid || a.grantedByUid || source.createdByUid || a.createdByUid || seller.uid || seller.id || ''),
15463      sellerEmail: String(source.sellerEmail || a.sellerEmail || source.consignatariaEmail || a.consignatariaEmail || source.ownerEmail || a.ownerEmail || source.authorizerEmail || a.authorizerEmail || source.grantedByEmail || a.grantedByEmail || source.createdByEmail || a.createdByEmail || seller.email || ''),
15464      sellerName: String(source.sellerName || a.sellerName || source.sellerDisplayName || a.sellerDisplayName || source.sellerFullName || a.sellerFullName || source.consignatariaName || a.consignatariaName || source.ownerName || a.ownerName || source.authorizerName || a.authorizerName || source.grantedByName || a.grantedByName || source.createdByName || a.createdByName || seller.displayName || seller.fullName || seller.name || sellerFullName || seller.nombreCompleto || (typeof source.consignataria === 'string' ?source.consignataria : '') || ''),
15465      isActive: source.isActive !== false && source.active !== false && source.activo !== false,
15466    };
15467  }
15468  normalizeCreditMovement(movement, index) {
15469    const source = movement?.movement || movement || {};
15470    const finite = (...values) => {
15471      const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== '' && Number.isFinite(Number(candidate)));
15472      return value === undefined ?0 : Number(value);
15473    };
15474    const rawStatus = String(source.status || source.state || source.movementStatus || source.result || '').trim().toLowerCase();
15475    const status = rawStatus === 'won' ?'won' : rawStatus === 'winning' ?'winning' : rawStatus;
15476    return {
15477      id: source.id || source.movementId || source.bidId || ('credit-movement-' + index),
15478      date: source.date || source.createdAt || source.occurredAt || source.updatedAt || source.timestamp || '',
15479      lot: source.lotName || source.lotLabel || source.lotTitle || source.category || source.categoria || (source.lotNumber != null ?'Lote ' + source.lotNumber : '') || source.lotCert || source.cert || source.lotId || 'Lote',
15480      status,
15481      bidAmount: finite(source.bidAmount, source.amount, source.offeredAmount, source.offerAmount, source.currentAmount),
15482      consumedAmount: finite(source.consumedCreditAmount, source.creditConsumed, source.creditAmount, source.usedCreditAmount, source.usedAmount),
15483    };
15484  }
15485  normalizeCreditSummary(data, authorization) {
15486    const envelope = data?.creditSummary || data?.summary || data?.result || data?.data || data || {};
15487    const source = Array.isArray(envelope) ?{} : envelope;
15488    const finiteOrNull = (...values) => {
15489      const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== '' && Number.isFinite(Number(candidate)));
15490      return value === undefined ?null : Number(value);
15491    };
15492    const movementSource = [source.movements, source.items, source.history, source.creditMovements, data?.movements, data?.items].find(Array.isArray) || [];
15493    const movements = movementSource.map((movement, index) => this.normalizeCreditMovement(movement, index));
15494    const weeklyCreditAmount = finiteOrNull(source.weeklyCreditAmount, source.authorizedCreditAmount, source.creditAmount, authorization?.weeklyCreditAmount) || 0;
15495    const usedFromMovements = movements.reduce((sum, movement) => sum + (Number(movement.consumedAmount) || 0), 0);
15496    const usedCreditAmount = finiteOrNull(source.usedCreditAmount, source.usedAmount, source.creditUsed, source.consumedCreditAmount);
15497    const normalizedUsed = usedCreditAmount === null ?usedFromMovements : usedCreditAmount;
15498    const availableCreditAmount = finiteOrNull(source.availableCreditAmount, source.availableAmou
15498nt, source.creditAvailable, source.remainingCreditAmount);
15499    return {
15500      weeklyCreditAmount,
15501      usedCreditAmount: Math.max(0, normalizedUsed || 0),
15502      availableCreditAmount: Math.max(0, availableCreditAmount === null ?weeklyCreditAmount - (normalizedUsed || 0) : availableCreditAmount),
15503      movements,
15504    };
15505  }
15506  creditAuthorizationKey(authorization) {
15507    const normalized = this.normBuyerAuth(authorization) || authorization || {};
15508    return String(normalized.summaryId || normalized.authorizationId || normalized.id || '').trim();
15509  }
15510  creditAuthorizationIsCurrent(authorization) {
15511    const normalized = this.normBuyerAuth(authorization) || authorization || {};
15512    if (normalized.isActive === false) return false;
15513    const today = this.localIsoDate();
15514    const start = String(normalized.creditStartDate || '').slice(0, 10);
15515    const end = String(normalized.creditEndDate || this.calculateCreditEndDate(start) || '').slice(0, 10);
15516    return (!start || start <= today) && (!end || end >= today);
15517  }
15518  async loadCreditSummaryIntoCache(authorization, { force = false } = {}) {
15519    const normalized = this.normBuyerAuth(authorization) || authorization || {};
15520    const authorizationId = this.creditAuthorizationKey(normalized);
15521    if (!authorizationId) return null;
15522    const cached = this.state.creditSummaryCache?.[authorizationId];
15523    if (cached && !force) return cached;
15524    this._creditSummaryPromises = this._creditSummaryPromises || {};
15525    if (this._creditSummaryPromises[authorizationId]) return this._creditSummaryPromises[authorizationId];
15526    const request = (async () => {
15527      this.setState((state) => ({
15528        creditSummaryLoadingIds: { ...(state.creditSummaryLoadingIds || {}), [authorizationId]: true },
15529        creditSummaryErrors: { ...(state.creditSummaryErrors || {}), [authorizationId]: '' },
15530      }));
15531      try {
15532        const data = await this.apiFetch('/api/BuyerAuthorizations/' + encodeURIComponent(authorizationId) + '/credit-summary');
15533        const summary = this.normalizeCreditSummary(data, normalized);
15534        this.setState((state) => ({
15535          creditSummaryCache: { ...(state.creditSummaryCache || {}), [authorizationId]: summary },
15536          creditSummaryLoadingIds: { ...(state.creditSummaryLoadingIds || {}), [authorizationId]: false },
15537          creditSummaryErrors: { ...(state.creditSummaryErrors || {}), [authorizationId]: '' },
15538        }));
15539        return summary;
15540      } catch (error) {
15541        const message = error?.message || 'No se pudo cargar el historial del crédito.';
15542        this.setState((state) => ({
15543          creditSummaryLoadingIds: { ...(state.creditSummaryLoadingIds || {}), [authorizationId]: false },
15544          creditSummaryErrors: { ...(state.creditSummaryErrors || {}), [authorizationId]: message },
15545        }));
15546        throw error;
15547      } finally {
15548        delete this._creditSummaryPromises[authorizationId];
15549      }
15550    })();
15551    this._creditSummaryPromises[authorizationId] = request;
15552    return request;
15553  }
15554  prefetchCurrentCreditSummaries(authorizations) {
15555    (authorizations || [])
15556      .filter((authorization) => this.creditAuthorizationIsCurrent(authorization))
15557      .forEach((authorization) => this.loadCreditSummaryIntoCache(authorization).catch(() => {}));
15558  }
15559  async refreshCreditRealtime({ refreshAuthorizations = false } = {}) {
15560    if (this._creditRealtimeSyncing || document.hidden || this.state.route !== 'profile' || !this.state.user) return;
15561    const canManage = this.canManageBuyerAccounts(this.state.apiUser);
15562    if (!canManage) return;
15563    this._creditRealtimeSyncing = true;
15564    try {
15565      let buyerAuths = this.state.buyerAuths || [];
15566      if (refreshAuthorizations || !this._lastCreditAuthorizationRefreshAt || Date.now() - this._lastCreditAuthorizationRefreshAt > 12000) {
15567        buyerAuths = await this.fetchBuyerAuths();
15568        buyerAuths = Array.isArray(buyerAuths) ?buyerAuths : (this.state.buyerAuths || []);
15569        this._lastCreditAuthorizationRefreshAt = Date.now();
15570      }
15571      const current = buyerAuths
15572        .filter((authorization) => this.creditAuthorizationIsCurrent(authorization));
15573      const unique = [...new Map(current.map((authorization) => [this.creditAuthorizationKey(authorization), authorization])).values()]
15574        .filter((authorization) => this.creditAuthorizationKey(authorization));
15575      await Promise.allSettled(unique.map((authorization) => this.loadCreditSummaryIntoCache(authorization, { force: true })));
15576    } finally {
15577      this._creditRealtimeSyncing = false;
15578    }
15579  }
15580  openCreditHistory(authorization = null, perspective = 'buyer') {
15581    if (perspective !== 'seller' || !this.canManageBuyerAccounts(this.state.apiUser)) return;
15582    this.setState({
15583      creditHistoryOpen: true,
15584      creditHistoryPerspective: 'seller',
15585      creditHistoryAuthorization: authorization ?(this.normBuyerAuth(authorization) || authorization) : null,
15586    });
15587  }
15588  closeCreditHistory() {
15589    this.setState({ creditHistoryOpen: false, creditHistoryAuthorization: null });
15590  }
15591  async openCreditSummary(authorization, perspective) {
15592    if (perspective !== 'seller' || !this.canManageBuyerAccounts(this.state.apiUser)) return;
15593    const normalized = this.normBuyerAuth(authorization) || authorization || {};
15594    const authorizationId = normalized.summaryId || normalized.authorizationId || normalized.id;
15595    if (!authorizationId) {
15596      this.showToast('La autorización no incluye un identificador para consultar el historial.');
15597      return;
15598    }
15599    const cached = this.state.creditSummaryCache?.[authorizationId] || null;
15600    this.setState({
15601      creditHistoryOpen: false,
15602      creditSummaryOpen: true,
15603      creditSummaryLoading: !cached,
15604      creditSummaryError: '',
15605      creditSummary: cached,
15606      creditSummaryAuthorization: normalized,
15607      creditSummaryPerspective: 'seller',
15608    });
15609    if (cached) return;
15610    try {
15611      const summary = await this.loadCreditSummaryIntoCache(normalized);
15612      this.setState({ creditSummary: summary, creditSummaryLoading: false, creditSummaryError: '' });
15613    } catch (error) {
15614      this.setState({ creditSummary: null, creditSummaryLoading: false, creditSummaryError: error?.message || 'No se pudo cargar el historial del crédito.' });
15615    }
15616  }
15617  closeCreditSummary() {
15618    this.setState({ creditSummaryOpen: false, creditSummaryLoading: false, creditSummaryError: '', creditSummary: null, creditSummaryAuthorization: null });
15619  }
15620  refreshCreditSummary() {
15621    const authorization = this.state.creditSummaryAuthorization;
15622    if (!authorization || this.state.creditSummaryLoading) return;
15623    this.setState({ creditSummaryLoading: true, creditSummaryError: '' });
15624    this.loadCreditSummaryIntoCache(authorization, { force: true })
15625      .then((summary) => this.setState({ creditSummary: summary, creditSummaryLoading: false, creditSummaryError: '' }))
15626      .catch((error) => this.setState({ creditSummaryLoading: false, creditSummaryError: error?.message || 'No se pudo actualizar el historial del crédito.' }));
15627  }
15628  normBuyerAccount(user) {
15629    if (!user) return null;
15630    const roles = this.userRoleIds(user);
15631    if (!roles.includes('buyer') || roles.includes('consignatario')) return null;
15632    const uid = String(user.uid || user.userId || user.id || '');
15633    const email = String(user.email || '');
15634    if (!uid && !email) return null;
15635    return {
15636      uid,
15637      email,
15638      displayName: user.displayName || user.name || email.split('@')[0] || 'Comprador',
15639      photoUrl: user.photoUrl || user.photoURL || '',
15640      emailVerified: user.emailVerified === true,
15641      providers: Array.isArray(user.providers) ?user.providers : [],
15642      createdAt: user.createdAt || '',
15643      lastLoginAt: user.lastLoginAt || '',
15644    };
15645  }
15646  async fetchBuyerAccounts() {
15647    if (this.state.buyerAccountsLoading) return;
15648    this.setState({ buyerAccountsLoading: true, buyerAccountsError: '' });
15649    try {
15650      const raw = await this.fetchAllPages('/api/Users/buyers', ['data', 'items', 'users', 'buyers']);
15651      const accounts = raw.map((user) => this.normBuyerAccount(user)).filter(Boolean);
15652      this.setProfileFetchInfo('cuentas comprador', accounts.length, '');
15653      this.setState({ buyerAccounts: accounts, buyerAccountsLoading: false, buyerAccountsError: '' });
15654    } catch (e) {
15655      if (e?.status === 403) {
15656        this.setProfileFetchInfo('cuentas comprador', 0, '');
15657        this.setState({ buyerAccounts: [], buyerAccountsLoading: false, buyerAccountsError: '' });
15658        return;
15659      }
15660      this.setProfileFetchInfo('cuentas comprador', null, e?.message || 'Error');
15661      this.setState({ buyerAccounts: [], buyerAccountsLoading: false, buyerAccountsError: e?.message || 'No se pudieron cargar las cuentas comprador.' });
15662    }
15663  }
15664  authorizeBuyerAccount(account) {
15665    this.setState({
15666      baFormOpen: true,
15667      baForm: { ...this.emptyBaForm(), buyerUid: account.uid || '', buyerEmail: account.email || '', creditStartDate: this.localIsoDate() },
15668      baFormMessage: null,
15669    }, () => requestAnimationFrame(() => document.getElementById('buyer-auth-form')?.scrollIntoView({ behavior: 'smooth', block: 'center' })));
15670  }
15671  async fetchBuyerAuths() {
15672    if (this._buyerAuthsFetching) return this.state.buyerAuths || [];
15673    this._buyerAuthsFetching = true;
15674    const isInitialLoad = this.state.buyerAuths === null;
15675    if (isInitialLoad) this.setState({ buyerAuthsLoading: true, buyerAuthsError: '' });
15676    try {
15677      const data = await this.apiFetch('/api/BuyerAuthorizations/mine?limit=50');
15678      const arr = this.listFrom(data, ['items', 'data', 'authorizations']);
15679      const list = arr.map((a) => this.normBuyerAuth(a)).filter((a) => a && (a.id || a.buyerEmail));
15680      this.setProfileFetchInfo('autorizaciones', list.length, '');
15681      this.setState({ buyerAuths: list, buyerAuthsLoading: false, buyerAuthsError: '' });
15682      this.prefetchCurrentCreditSummaries(list);
15683      return list;
15684    } catch (e) {
15685      this.setProfileFetchInfo('autorizaciones', null, e?.message || 'Error');
15686      this.setState({
15687        buyerAuths: isInitialLoad ?[] : this.state.buyerAuths,
15688        buyerAuthsLoading: false,
15689        buyerAuthsError: e?.message || 'No se pudieron cargar autorizaciones.',
15690      });
15691      return this.state.buyerAuths || [];
15692    } finally {
15693      this._buyerAuthsFetching = false;
15694    }
15695  }
15696  async fetchBuyerAccess() {
15697    // El crédito es una herramienta privada del vendedor. El comprador no
15698    // consulta autorizaciones ni resúmenes para evitar exponer montos en red,
15699    // estado de React o DOM.
15700    this.setState({ buyerAccess: [], buyerAccessLoading: false, buyerAccessError: '' });
15701    return [];
15702  }
15703  async hydrateBuyerSellerLabels(authorizations) {
15704    const current = this.state.sellerLabels || {};
15705    const sellerUids = [...new Set((authorizations || [])
15706      .map((authorization) => String(authorization?.sellerUid || '').trim())
15707      .filter((uid) => uid && !current[uid]))];
15708    if (!sellerUids.length) return;
15709    const resolved = await Promise.all(sellerUids.map(async (uid) => {
15710      try {
15711        const data = await this.apiFetch('/api/user-labels/' + encodeURIComponent(uid));
15712        const source = data?.data || data;
15713        const displayName = String(source?.displayName || source?.name || '').trim();
15714        return displayName ?[uid, displayName] : null;
15715      } catch {
15716        return null;
15717      }
15718    }));
15719    const labels = Object.fromEntries(resolved.filter(Boolean));
15720    if (Object.keys(labels).length) {
15721      this.setState((state) => ({ sellerLabels: { ...(state.sellerLabels || {}), ...labels } }));
15722    }
15723  }
15724  setBaForm(key, val) { this.setState({ baForm: { ...(this.state.baForm || this.emptyBaForm()), [key]: val }, baFormMessage: null }); }
15725  async saveBuyerAuth() {
15726    if (this.state.baSaving) return;
15727    const f = this.state.baForm || this.emptyBaForm();
15728    if (!String(f.buyerEmail).trim()) { this.setState({ baFormMessage: { text: 'Seleccioná un comprador del directorio.', ok: false } }); return; }
15729    if (!String(f.buyerUid || '').trim() || !(this.state.buyerAccounts || []).some((account) => account.uid === f.buyerUid && account.email === f.buyerEmail)) { this.setState({ baFormMessage: { text: 'Seleccioná una cuenta con rol comprador del directorio actualizado.', ok: false } }); return; }
15730    if (!String(f.creditStartDate || '').trim()) { this.setState({ baFormMessage: { text: 'Indicá el inicio del crédito.', ok: false } }); return; }
15731    const weeklyCreditAmount = this.numOrNull(f.weeklyCreditAmount);
15732    if (!(weeklyCreditAmount > 0)) { this.setState({ baFormMessage: { text: 'El crédito semanal debe ser mayor a $0.', ok: false } }); return; }
15733    const body = {
15734      buyerUid: String(f.buyerUid || '').trim(),
15735      buyerEmail: String(f.buyerEmail).trim(),
15736      creditStartDate: String(f.creditStartDate).trim(),
15737      weeklyCreditAmount,
15738    };
15739    this.setState({ baSaving: true, baFormMessage: { text: 'Creando crédito semanal…', ok: true } });
15740    try {
15741      await this.apiFetch('/api/BuyerAuthorizations', { method: 'POST', body: JSON.stringify(body) });
15742      await this.fetchBuyerAuths();
15743      this.setState({ baSaving: false, baFormOpen: false, baForm: this.emptyBaForm(), baFormMessage: null });
15744      this.showToast('Crédito semanal creado para "' + body.buyerEmail + '".');
15745    } catch (error) {
15746      console.error('[Roslot] No se pudo crear el crédito semanal', { status: error?.status || null, message: error?.message || '' });
15747      const detail = error?.message || 'No se pudo crear el crédito.';
15748      const status = Number(error?.status);
15749      this.setState({ baSaving: false, baFormMessage: { text: status && !detail.includes('HTTP ' + status) ?'HTTP ' + status + ' · ' + detail : detail, ok: false } });
15750    }
15751  }
15752  async pushBuyerAuth(id) {
15753    const cur = (this.state.buyerAuths || []).find((a) => a.id === id); if (!cur) return;
15754    if (!cur.creditStartDate || !(Number(cur.weeklyCreditAmount) > 0)) { this.showToast('Completá una fecha de inicio y un monto mayor a $0.'); return; }
15755    this.setState({ baActioning: id });
15756    try {
15757      await this.apiFetch('/api/BuyerAuthorizations/' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify({ creditStartDate: cur.creditStartDate, weeklyCreditAmount: Number(cur.weeklyCreditAmount), isActive: cur.isActive }) });
15758      await this.fetchBuyerAuths();
15759      this.showToast('Crédito actualizado.');
15760    } catch { this.showToast('No se pudo actualizar el crédito.'); }
15761    this.setState({ baActioning: '' });
15762  }
15763  updateBuyerAuthField(id, key, value) {
15764    const list = this.state.buyerAuths || [];
15765    const cur = list.find((a) => a.id === id); if (!cur) return;
15766    const next = { ...cur, [key]: key === 'weeklyCreditAmount' ?String(value).replace(/\D/g, '').slice(0, 15) : value };
15767    if (key === 'creditStartDate') next.creditEndDate = '';
15768    this.setState({ buyerAuths: list.map((a) => a.id === id ?next : a) });
15769  }
15770  toggleBaActive(id) {
15771    const list = this.state.buyerAuths || [];
15772    const cur = list.find((a) => a.id === id); if (!cur) return;
15773    const next = { ...cur, isActive: !cur.isActive };
15774    this.setState({ buyerAuths: list.map((a) => a.id === id ?next : a) });
15775    this._baDebounce = this._baDebounce || {};
15776    clearTimeout(this._baDebounce[id]);
15777    this._baDebounce[id] = setTimeout(() => this.pushBuyerAuth(id), 250);
15778  }
15779  async deleteBuyerAuth(id) {
15780    if (!id || this.state.baActioning) return;
15781    this.setState({ baActioning: id });
15782    try {
15783      await this.apiFetch('/api/BuyerAuthorizations/' + encodeURIComponent(id), { method: 'DELETE' });
15784      this.setState({ buyerAuths: (this.state.buyerAuths || []).filter((a) => a.id !== id), baActioning: '' });
15785      this.showToast('Autorización eliminada.');
15786    } catch { this.setState({ baActioning: '' }); this.showToast('No se pudo eliminar.'); }
15787  }
15788  setEst(id) {
15789    const f = this.form();
15790    if (!id) { this.setState({ form: { ...f, estId: '' }, formMessage: null }); return; }
15791    const e = (this.state.establishments || []).find((x) => x.id === id);
15792    if (!e) { this.setState({ form: { ...f, estId: '' }, formMessage: null }); return; }
15793    this.setState({ form: { ...f, estId: id, estName: e.nombre, comitente: e.comitente, prov: e.provincia, loc: e.localidad, lat: e.latitud != null ?String(e.latitud) : '', lon: e.longitud != null ?String(e.longitud) : '' }, formMessage: null });
15794  }
15795  mapLot(l, i) {
15796    const cert = String(l.cert || l.lotCert || l.certificate || l.certificado || l.id || String(l.lotNumber || i + 1).padStart(4, '0'));
15797    const razas = (l.razas || []).map((r) =>
15797 r.name || r).filter(Boolean);
15798    const rawVideos = l.videos || l.videoAssets || l.videoUploads || l.videoUrls || [];
15799    const videos = (Array.isArray(rawVideos) ?rawVideos : []).map((v) => ({
15800      url: typeof v === 'string' ?v : (v.url || v.publicUrl || v.downloadUrl || v.videoUrl || v.src || ''),
15801      thumb: typeof v === 'object' ?(v.thumbnailUrl || v.thumbnail || v.posterUrl || v.poster || v.coverUrl || v.frameUrl || v.previewUrl || '') : '',
15802    })).filter((v) => v.url);
15803    const singleVideoUrl = l.videoUrl || l.video || '';
15804    if (!videos.length && singleVideoUrl) videos.push({ url: singleVideoUrl, thumb: '' });
15805    const dateRaw = l.fechaSubasta || '';
15806    const dateParts = String(dateRaw).slice(0, 10).split('-').map(Number);
15807    const d = dateParts.length === 3 && dateParts.every(Number.isFinite)
15808       ? new Date(dateParts[0], dateParts[1] - 1, dateParts[2])
15809      : (dateRaw ?new Date(dateRaw) : null);
15810    const today = new Date();
15811    const todayKey = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
15812    const dateKey = d && !isNaN(d) ?new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() : null;
15813    const isToday = dateKey !== null && dateKey === todayKey;
15814    const isFuture = dateKey !== null && dateKey > todayKey;
15815    const rawAuctionStatus = String(l.auctionStatus || l.auction_state || l.auctionState || '').trim().toLowerCase();
15816    const rawStatus = String(l.status || l.estado  || l.state || '').trim().toLowerCase();
15817    const rawBids = l.bids || l.ofertas || [];
15818    const bidCount = Number(l.bidCount || l.bidsCount  || l.totalBids || l.offerCount || l.totalOffers  || l.ofertasCount || rawBids.length) || 0;
15819    const locallyClosed = this.state.closedRoslotLots?.[cert] || null;
15820    const statusSignals = [rawAuctionStatus, rawStatus].join(' ');
15821    const explicitSale = !!(l.soldAt || l.saleClosedAt || l.adjudicatedAt || l.winnerBidId || l.winningBidId || l.winnerBuyerUid || l.winningBuyerUid || Number(l.winnerAmount || l.winningBidAmount || l.soldAmount) > 0);
15822    const hasBidEvidence = !!(locallyClosed || l.currentBidId || l.highestBidId || l.currentBidBuyerUid || l.highestBidBuyerUid || l.winnerBidId || l.winningBidId || l.winnerBuyerUid || l.winningBuyerUid || Number(l.currentBidAmount || l.highestBidAmount || l.winningBidAmount || l.soldAmount) > 0 || bidCount > 0);
15823    const closedSignal = l.isClosed === true || l.closed === true || !!l.closedAt || /closed|cerrad|finaliz|finished|ended/.test(statusSignals);
15824    const auctionStatus = locallyClosed || explicitSale || /sold|vendid|adjudic/.test(statusSignals)
15825      ? 'sold'
15826      : /closed[_ -]?no[_ -]?bids|sin[_ -]?ofertas|caduc|expired|vencid/.test(statusSignals)
15827        ? 'closed_no_bids'
15828        : closedSignal
15829          ? (hasBidEvidence ?'sold' : 'closed_no_bids')
15830          : /open|abiert|en vivo|live/.test(statusSignals)
15831            ? 'open'
15832            : /scheduled|program|upcoming|pr[oó]xim/.test(statusSignals)
15833              ? 'scheduled'
15834              : rawAuctionStatus;
15835    const auctionStatusMap = {
15836      scheduled: 'Próximo',
15837      open: 'En vivo',
15838      closed_no_bids: 'Caducado',
15839      sold: 'Vendido',
15840    };
15841    const legacyStatus = /expired|vencid|caduc|closed_no_bids/.test(rawStatus)
15842       ? 'Caducado'
15843      : /sold|vendid/.test(rawStatus)
15844         ? 'Vendido'
15845        : isToday
15846           ? 'En vivo'
15847          : isFuture
15848             ? 'Próximo'
15849            : (dateKey !== null && dateKey < todayKey)
15850               ? 'Caducado'
15851              : 'Publicado';
15852    const displayStatus = auctionStatusMap[auctionStatus] || legacyStatus;
15853    const isClosed = ['closed_no_bids', 'sold'].includes(auctionStatus) || closedSignal;
15854    const isExpired = displayStatus === 'Caducado';
15855    const hasOffers = auctionStatus === 'sold' || l.hasOffers === true || bidCount > 0;
15856    return {
15857      raw: l,
15858      cert,
15859      status: prebid.targets?.length && prebid.isWinner ?'Con animales ganadores':displayStatus,
15860      categoria: l.categoriaNombre || l.categoriaId || 'Sin categoría',
15861      categoriaSexType: String(l.categoriaSexType || ''),
15862      subcategoria: l.subcategoriaNombre || l.subcategoriaId || '',
15863      cabezas: l.totalCabezas || l.cantidadTotal  || '?',
15864      pesoNum: Number(l.pesoPromedio) || 0,
15865      peso: l.pesoPromedio ?l.pesoPromedio + ' kg' : 'Pendiente',
15866      baseNum: Number(l.precioSugerido) || 0,
15867      condicion: l.condicionNombre || l.condicionId || 'Condición pendiente',
15868      destino: l.destinoNombre || l.destinoId || 'Pendiente',
15869      engorde: l.engordeNombre || l.engordeId || 'Pendiente',
15870      plazoId: String(l.plazoId || ''),
15871      plazoOtro: String(l.plazoOtro || '').trim(),
15872      plazoEsOtro: [l.plazoId, l.plazoNombre].some((value) => String(value || '').trim().toLowerCase() === 'otro'),
15873      plazo: [l.plazoId, l.plazoNombre].some((value) => String(value || '').trim().toLowerCase() === 'otro') ?'Otro' : (l.plazoNombre || l.plazoId || 'Pendiente'),
15874      aptoChina: l.aptoChina === true || l.aptoChina === 'true',
15875      razas, provincia: l.establecimientoProvincia || l.provincia || '',
15876      origen: [l.establecimientoLocalidad || l.localidad, l.establecimientoProvincia || l.provincia].filter(
15876Boolean).join(', ') || 'Origen pendiente',
15877      image: l.coverImage?.url || l.thumbnailUrl || videos[0]?.thumb || '',
15878      videos, videoPreviewUrl: videos[0]?.url || '',
15879      views: Number(l.views) || 0,
15880      seller: l.sellerName || l.sellerEmail || 'Productor Rosgan',
15881      sellerUid: String(l.sellerUid || l.sellerId || l.createdByUid || l.userId || ''),
15882      sellerEmail: String(l.sellerEmail || l.ownerEmail || l.createdByEmail || l.userEmail || ''),
15883      consignatariaId: String(l.consignatariaId || l.sellerConsignatariaId || l.companyId || ''),
15884      consignatariaName: String(l.consignatariaNombre || l.consignatariaName || l.consignataria || l.sellerCompany || ''),
15885      fecha: dateRaw, fechaDisponibleCarga: String(l.fechaDisponibleCarga || '').slice(0, 10), createdAt: l.createdAt || '',
15886      republishCount: Number(l.republishCount) || 0,
15887      auctionStatus, rawStatus, isClosed, hasOffers, bidCount,
15888      currentBidAmount: Number(l.currentBidAmount || l.highestBidAmount  || l.topBidAmount || 0) || 0,
15889      currentBidBuyerUid: String(l.currentBidBuyerUid || l.highestBidBuyerUid  || l.leadingBuyerUid || ''),
15890      currentBidId: String(l.currentBidId  || l.highestBidId || ''),
15891      offerStartAt: l.offerStartAtUtc  || l.offerStartAt || null,
15892      offerEndAt: l.offerEndAtUtc  || l.offerEndAt || null,
15893      soldAmount: Number(locallyClosed?.amount || l.winningBidAmount || l.winnerAmount  || l.acceptedBidAmount || l.soldAmount || l.salePrice  || l.precioVenta || 0) || 0,
15894      winnerBidId: String(l.winnerBidId  || l.winningBidId || ''),
15895      winnerBuyerUid: String(l.winnerBuyerUid  || l.winningBuyerUid || ''),
15896      canRepublish: auctionStatus === 'closed_no_bids' || (!auctionStatus && isExpired && !hasOffers),
15897      establecimiento: l.establecimientoNombre || 'Pendiente',
15898      localidad: l.establecimientoLocalidad || l.localidad || 'Pendiente',
15899      comitente: l.establecimientoComitente || l.comitente || 'Pendiente',
15900      pesoMin: l.pesoMinimo ?l.pesoMinimo + ' kg' : 'Pendiente', pesoMax: l.pesoMaximo ?l.pesoMaximo + ' kg' : 'Pendiente',
15901      pesaje: l.pesajeNombre || 'Pendiente', desbaste: l.desbaste != null ?l.desbaste + '%' : 'Pendiente',
15902      rendimiento: l.rendimientoEstimado != null ?l.rendimientoEstimado + '%' : 'Pendiente',
15903      obs: l.observaciones || 'Sin observaciones cargadas.',
15904      bids: rawBids.map((b) => Number(b.amount || b.monto) || 0),
15905    };
15906  }
15907  roslotMarketTiming(lots = [], nowMs = Date.now()) {
15908    const parseTime = (value) => {
15909      const time = value ?new Date(value).getTime() : NaN;
15910      return Number.isFinite(time) ?time : null;
15911    };
15912    const windows = (lots || []).map((lot) => ({
15913      status: String(lot?.auctionStatus || '').toLowerCase(),
15914      start: parseTime(lot?.offerStartAt || lot?.raw?.offerStartAtUtc || lot?.raw?.offerStartAt),
15915      end: parseTime(lot?.offerEndAt || lot?.raw?.offerEndAtUtc || lot?.raw?.offerEndAt),
15916    })).filter((window) => window.start || window.end);
15917    const active = windows.filter((window) => window.status === 'open' && (!window.start || window.start <= nowMs) && (!window.end || window.end > nowMs));
15918    if (active.length) {
15919      const starts = active.map((window) => window.start).filter(Number.isFinite);
15920      const ends = active.map((window) => window.end).filter(Number.isFinite);
15921      const start = starts.length ?Math.min(...starts) : null;
15922      const end = ends.length ?Math.max(...ends) : null;
15923      const progress = start && end && end > start ?Math.min(100, Math.max(0, ((nowMs - start) / (end - start)) * 100)) : 100;
15924      return { isOpen: true, target: end, progress, hasTarget: !!end };
15925    }
15926    // El mercado tiene horario propio aunque no haya lotes publicados. Tomamos
15927    // la ventana completa mas reciente como calendario diario; el respaldo
15928    // coincide con el horario UTC que actualmente entrega el backend (11–20).
15929    const scheduleWindow = windows
15930      .filter((window) => window.start && window.end && window.end > window.start)
15931      .sort((a, b) => b.end - a.end)[0];
15932    const scheduleStart = new Date(scheduleWindow?.start || Date.UTC(2000, 0, 1, 11));
15933    const scheduleEnd = new Date(scheduleWindow?.end || Date.UTC(2000, 0, 1, 20));
15934    const baParts = Object.fromEntries(new Intl.DateTimeFormat('en-CA', {
15935      timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit',
15936    }).formatToParts(new Date(nowMs)).filter((part) => part.type !== 'literal').map((part) => [part.type, Number(part.value)]));
15937    const baDayUtc = Date.UTC(baParts.year, baParts.month - 1, baParts.day);
15938    const startOffset = scheduleStart.getUTCHours() * 3600000 + sc
15938heduleStart.getUTCMinutes() * 60000 + scheduleStart.getUTCSeconds() * 1000;
15939    let endOffset = scheduleEnd.getUTCHours() * 3600000 + scheduleEnd.getUTCMinutes() * 60000 + scheduleEnd.getUTCSeconds() * 1000;
15940    if (endOffset <= startOffset) endOffset += 24 * 60 * 60 * 1000;
15941    const dailyStart = baDayUtc + startOffset;
15942    const dailyEnd = baDayUtc + endOffset;
15943    if (nowMs >= dailyStart && nowMs < dailyEnd) {
15944      const progress = Math.min(100, Math.max(0, ((nowMs - dailyStart) / (dailyEnd - dailyStart)) * 100));
15945      return { isOpen: true, target: dailyEnd, progress, hasTarget: true, scheduleFallback: true };
15946    }
15947    const nextStarts = windows
15948      .filter((window) => !['sold', 'closed_no_bids'].includes(window.status) && window.start && window.start > nowMs)
15949      .map((window) => window.start);
15950    const scheduledTarget = nowMs < dailyStart ?dailyStart : dailyStart + 24 * 60 * 60 * 1000;
15951    const target = Math.min(scheduledTarget, ...(nextStarts.length ?nextStarts : [scheduledTarget]));
15952    const pastEnds = windows.map((window) => window.end).filter((end) => end && end <= nowMs && end < target);
15953    const dailyLastClose = nowMs < dailyStart ?dailyEnd - 24 * 60 * 60 * 1000 : dailyEnd;
15954    const lastClose = Math.max(dailyLastClose, ...(pastEnds.length ?pastEnds : [dailyLastClose]));
15955    const progressStart = Math.max(lastClose || 0, target - 24 * 60 * 60 * 1000);
15956    const progress = target > progressStart ?Math.min(100, Math.max(0, ((nowMs - progressStart) / (target - progressStart)) * 100)) : 0;
15957    return { isOpen: false, target, progress, hasTarget: true, scheduleFallback: true };
15958  }
15959  updateRoslotMarketClockDom(nowMs = Date.now()) {
15960    if (!['pool', 'lot'].includes(this.state.route)) return;
15961    const lots = this.state.lots || [];
15962    const timing = this.roslotMarketTiming(lots, nowMs);
15963    const baDateFormatter = this._roslotBaDateFormatter || (this._roslotBaDateFormatter = new Intl.DateTimeFormat('en-CA', {
15964      timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit',
15965    }));
15966    const baTodayKey = baDateFormatter.format(new Date(nowMs));
15967    const lotDay = (lot) => String(lot?.fecha || lot?.raw?.fechaSubasta || '').slice(0, 10);
15968    const marketOpen = lots.some((lot) => lot.auctionStatus === 'open' && lotDay(lot) === baTodayKey) || timing.isOpen;
15969    const target = marketOpen ?(timing.isOpen ?timing.target : null) : timing.target;
15970    const progress = marketOpen && !timing.isOpen ?100 : Math.round(Number(timing.progress || 0) * 10) / 10;
15971    const remaining = (value) => {
15972      const seconds = Math.max(0, Math.ceil((value - nowMs) / 1000));
15973      const days = Math.floor(seconds / 86400);
15974      const hours = Math.floor((seconds % 86400) / 3600);
15975      const minutes = Math.floor((seconds % 3600) / 60);
15976      const secs = seconds % 60;
15977      if (days) return `${days} d ${String(hours).padStart(2, '0')} h`;
15978      return `${String(hours).padStart(2, '0')}
15978:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
15979    };
15980    const scheduleFormatter = this._roslotScheduleFormatter || (this._roslotScheduleFormatter = new Intl.DateTimeFormat('es-AR', {
15981      timeZone: 'America/Argentina/Buenos_Aires', weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
15982    }));
15983    const targetLabel = target ?scheduleFormatter.format(new Date(target)).replace(',', ' ·') : '';
15984    const countdown = this.state.lotsLoading ?'Sincronizando horarios…' : (marketOpen ?(target ?'Cierra en ' + remaining(target) : 'Operando ahora') : (target ?'Abre en ' + remaining(target) : 'Sin subastas programadas'));
15985    const schedule = this.state.lotsLoading ?'Consultando la próxima ventana del mercado' : (marketOpen ?(target ?'Cierre previsto: ' + targetLabel : 'Mercado operando en este momento') : (target ?'Próxima apertura: ' + targetLabel : 'Próxima apertura sin programar'));
15986    const countdownNodes = document.querySelectorAll('[data-roslot-market-countdown]');
15987    const scheduleNodes = document.querySelectorAll('[data-roslot-market-schedule]');
15988    const progressNodes = document.querySelectorAll('[data-roslot-market-progress]');
15989    const fillNodes = document.querySelectorAll('[data-roslot-market-progress-fill]');
15990    countdownNodes.forEach((countdownNode) => {
15991      countdownNode.textContent = countdown;
15992      countdownNode.style.color = marketOpen ?'#176c58' : target ?'#07589d' : '#687a8f';
15993    });
15994    scheduleNodes.forEach((scheduleNode) => { scheduleNode.textContent = schedule; });
15995    progressNodes.forEach((progressNode) => { progressNode.setAttribute('aria-valuenow', String(progress)); });
15996    fillNodes.forEach((fillNode) => {
15997      fillNode.style.width = progress + '%';
15998      fillNode.style.background = marketOpen ?'linear-gradient(90deg,#1f7a63,#43b48f)' : target ?'linear-gradient(90deg,#075da8,#28a7d8)' : '#aebdca';
15999    });
16000  }
16001  fmtARS(n) { return '$' + Math.round(n).toLocaleString('es-AR'); }
16002  moneyInputRaw(value, maxLength = 15) {
16003    return String(value ?? '').replace(/\D/g, '').slice(0, maxLength);
16004  }
16005  moneyInputDisplay(value) {
16006    const digits = this.moneyInputRaw(value).replace(/^0+(?=\d)/, '');
16007    return digits ?digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.') : '';
16008  }
16009  formatMoneyInputEvent(event, onRaw) {
16010    const input = event?.target;
16011    const value = String(input?.value || '');
16012    const caret = Number.isFinite(input?.selectionStart) ?input.selectionStart : value.length;
16013    const digitsBeforeCaret = this.moneyInputRaw(value.slice(0, caret)).length;
16014    const raw = this.moneyInputRaw(value);
16015    const formatted = this.moneyInputDisplay(raw);
16016    const applyFormattedValue = () => {
16017      if (!input || !input.isConnected) return;
16018      input.value = formatted;
16019      let nextCaret = formatted.length;
16020      if (digitsBeforeCaret < raw.length) {
16021        let seen = 0;
16022        nextCaret = 0;
16023        while (nextCaret < formatted.length && seen < digitsBeforeCaret) {
16024          if (/\d/.test(formatted[nextCaret])) seen += 1;
16025          nextCaret += 1;
16026        }
16027      }
16028      try { input.setSelectionRange(nextCaret, nextCaret); } catch {}
16029    };
16030    if (typeof onRaw === 'function') onRaw(raw);
16031    applyFormattedValue();
16032    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(applyFormattedValue);
16033  }
16034  cuitInputRaw(value) {
16035    return String(value ?? '').replace(/\D/g, '').slice(0, 11);
16036  }
16037  cuitInputDisplay(value) {
16038    const digits = this.cuitInputRaw(value);
16039    if (digits.length <= 2) return digits;
16040    if (digits.length <= 10) return digits.slice(0, 2) + '-' + digits.slice(2);
16041    return digits.slice(0, 2) + '-' + digits.slice(2, 10) + '-' + digits.slice(10);
16042  }
16043  apiUserCuit(profile = this.state.apiUser || {}) {
16044    const candidates = [
16045      profile?.cuitCuil, profile?.cuit, profile?.cuil,
16046      profile?.profile?.cuitCuil, profile?.profile?.cuit, profile?.profile?.cuil,
16047      profile?.user?.cuitCuil, profile?.user?.cuit, profile?.user?.cuil,
16048      profile?.userData?.cuitCuil, profile?.userData?.cuit, profile?.userData?.cuil,
16049      profile?.producer?.cuitCuil, profile?.producer?.cuit,
16050    ];
16051    return this.cuitInputRaw(candidates.find((value) => this.cuitInputRaw(value).length === 11) || '');
16052  }
16053  async persistCabanaPrebidCuit(value) {
16054    const digits = this.cuitInputRaw(value);
16055    if (digits.length !== 11) throw new Error('Ingresá un CUIT/CUIL de 11 dígitos.');
16056    const response = await this.cabanaApiFetch('/api/users/me',{method:'PATCH',body:JSON.stringify({cuitCuil:this.cuitInputDisplay(digits)})});
16057    const profile=response?.data ?? response;
16058    this.setState({cabanaPrebidProfile:profile,apiUser:{...this.state.apiUser,cuitCuil:profile.cuitCuil}});
16059    return profile;
16060  }
16061  rollText(str) {
16062    str = String(str || '');
16063    const R = React;
16064    return R.createElement('span', { style: { display: 'inline-flex', overflow: 'hidden' } },
16065      [...str].map((ch, i) => {
16066        if (!/\d/.test(ch)) return R.createElement('span', { key: 'c' + i }, ch);
16067        const d = Number(ch);
16068        return R.createElement('span', { key: 'd' + (str.length - i), style: { display: 'inline-block', height: '1.15em', overflow: 'hidden' } },
16069          R.createElement('span', { style: { display: 'flex', flexDirection: 'column', transform: 'translateY(' + (-d * 1.15) + 'em)', transition: 'transform 550ms cubic-bezier(0.22,0.72,0.18,1) ' + (i * 30) + 'ms' } },
16070            Array.from({ length: 10 }, (_, k) => R.createElement('span', { key: k, style: { height: '1.15em', lineHeight: '1.15em' } }, String(k)))));
16071      }));
16072  }
16073  rollNumber(amount) { return this.rollText(this.fmtARS(amount)); }
16074  rollInteger(amount) { return this.rollText(Math.max(0, Number(amount) || 0).toLocaleStr
16074ing('es-AR')); }
16075  emptyRemateConfig(type = '') {
16076    return { tipo: type || 'Remate Cabañas', titulo: '', cabezas: '', consignataria: '', provincia: '', lugar: '', localidad: '', fecha: '', hora: '', urlStreaming: '', contactoId: '', contactoNombre: '', contactoTelefono: '', contactoEmail: '', contactoNotas: '', cargaDatos: 'abierta', cargaLotes: 'abierta', approverUserIds: [], categoryPricing: [], preInicioFecha: '', preInicioHora: '', preFinFecha: '', preFinHora: '', catalogoPdfName: '', catalogoPdfSize: 0, catalogoPdfUrl: '', catalogoMediaId: '', cabanasPrincipales: [this.newCabanaConfigEntry()], cabanasInvitadas: [this.newCabanaConfigEntry()], formulariosCategoria: [this.newRemateCategoryForm()] };
16077  }
16078  newCabanaConfigEntry() { return { id: 'cabana-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), nombre: '', logoName: '', logoDataUrl: '', logoWhiteDataUrl: '', brandColor: '' }; }
16079  newRemateFormField(order = 1) { return { id: 'campo-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), nombre: '', tipo: 'texto', opciones: '', requerido: false, identificadorPrincipal: false, identificadorCaracteristico: false, mostrarTarjeta: false, orden: order }; }
16080  newRemateSubform(displayOrder = 1) { return { id: 'form-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), templateId: null, nombre: 'Datos principales', orden: displayOrder, activo: true, campos: [this.newRemateFormField()] }; }
16081  newRemateCategoryForm() { return { id: 'categoria-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), categoryId: null, categoria: '', formularios: [this.newRemateSubform()] }; }
16082  normalizeRemateCategoryForm(category) {
16083    if (Array.isArray(category?.formularios)) return category;
16084    const legacyFields = Array.isArray(category?.campos) && category.campos.length ?category.campos : [this.newRemateFormField()];
16085    return { ...category, formularios: [{ id: 'form-legacy-' + category.id, templateId: null, nombre: 'Datos principales', orden: 1, activo: true, campos: legacyFields.map((field, index) => ({ ...field, orden: field.orden || index + 1 })) }], campos: undefined };
16086  }
16087  emptyRemateConfigToggles() {
16088    return { requerirRegistro: false, videoFinal: false, logoRural: false, preHabilitada: false, preIndividual: false, apruebaAuto: false, preAgrupada: false, agrupaCorrales: false };
16089  }
16090  remateLocalDateTimeParts(value) {
16091    if (!value) return { date: '', time: '' };
16092    const raw = String(value);
16093    if (/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)?$/.test(raw)) return {date:raw.slice(0,10),time:raw.slice(11,16)};
16094    const parsed = new Date(value);
16095    if (Number.isNaN(parsed.getTime())) return { date: '', time: '' };
16096    const parts = Object.fromEntries(new Intl.DateTimeFormat('en-GB', {timeZone:'America/Argentina/Buenos_Aires',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).formatToParts(parsed).map(part => [part.type,part.value]));
16097    return {date:parts.year + '-' + parts.month + '-' + parts.day,time:parts.hour + ':' + parts.minute};
16098  }
16099  openRemateConfig(type = 'Remate Cabañas', syncPath = true) {
16100    this.resetCabanaRematePlaces();
16101    this.setState({ rcSchemaEditor: false });
16102    this.fetchAdminUsers();
16103    if (!this.canCreateCabanaRemate()) { this.showToast('Solo admin y sysadmin pueden crear remates de cabaña.'); return; }
16104    this.fetchProvinces();
16105    const form = this.emptyRemateConfig(type);
16106    this.rememberRoute('remateConfig');
16107    this.setState({ route: 'remateConfig', remateConfigForm: form, remateConfigToggles: this.emptyRemateConfigToggles(), remateConfigSaved: false, remateConfigEditingId: '', remateConfigSaving: '' });
16108    if (syncPath) { try { this.writeAppHistory('/cabanas/crear'); } catch {} }
16109    window.scrollTo(0, 0);
16110  }
16111  remateConfigFromItem(item) {
16112    const fieldFromApi = (field, order) => ({
16113      id: field?.id || 'campo-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7),
16114      nombre: field?.name || field?.nombre || '', tipo: String(field?.type || field?.tipo || 'texto').toLowerCase(),
16115      opcionesOriginales: field?.options || field?.opciones || [],
16116      opciones: (field?.options || field?.opciones || []).map((option) => typeof option === 'string' ?option : option?.label || option?.name || option?.value || '').filter(
16116Boolean).join(', '),
16117      identificadorCaracteristico: field?.isCharacteristicIdentifier === true, requerido: field?.required === true, identificadorPrincipal: field?.isPrimaryIdentifier === true || field?.identificadorPrincipal === true,
16118      mostrarTarjeta: field?.showOnCard === true || field?.mostrarTarjeta === true, orden: Number(field?.order || field?.orden || order || 1),
16119    });
16120    const categories = this.cabanaRemateSchema(item).map((category, categoryIndex) => {
16121      const forms = [...(category.forms || [])].sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0));
16122      const formularios = forms.map((subform, subformIndex) => ({
16123        id: subform.id || 'form-' + categoryIndex + '-' + subformIndex,
16124        templateId: subform.templateId || null,
16125        nombre: subform.name || subform.nombre || 'Datos principales',
16126        orden: subformIndex + 1, activo: subform.isActive !== false && subform.activo !== false,
16127        campos: (subform.fields || subform.campos || []).map(fieldFromApi),
16128      }));
16129      return { id: 'categoria-' + categoryIndex, categoryId: category.categoryId || null, categoria: category.categoryName || category.categoria || '', formularios: formularios.length ?formularios : [this.newRemateSubform()] };
16130    });
16131    const schedule = item?.schedule || {}, location = item?.location || {}, load = item?.load || {}, catalog = item?.catalog || {}, contact = item?.contacto || item?.contact || {};
16132    const scheduledAt = this.remateLocalDateTimeParts(item?.fechaHoraInicio || item?.startAt || '');
16133    const prebidStart = this.remateLocalDateTimeParts(item?.prebid?.fechaHoraInicioPreoferta || item?.prebid?.inicio || '');
16134    const prebidEnd = this.remateLocalDateTimeParts(item?.prebid?.fechaHoraFinPreoferta || item?.prebid?.fin || '');
16135    const mapCabana = (cabana) => ({ id: cabana?.id || this.newCabanaConfigEntry().id, nombre: cabana?.nombre || cabana?.name || '', logoName: cabana?.logoName || '', logoDataUrl: window.resolveCabanaMediaUrl(cabana?.logoDataUrl || cabana?.logoUrl), logoWhiteDataUrl: window.resolveCabanaMediaUrl(cabana?.logoWhiteDataUrl), brandColor: cabana?.brandColor || '' });
16136    const participants = this.cabanaRemateParticipants(item);
16137    const principals = participants.filter((cabana) => cabana.type === 'principal').map(mapCabana), guests = participants.filter((cabana) => cabana.type !== 'principal').map(mapCabana);
16138    return {
16139      tipo: item?.tipo || 'Remate Cabañas', titulo: item?.title || item?.titulo || item?.nombre_remate || '', cabezas: item?.cabezasEstimadas  || item?.cabezas || '',
16140      consignataria: item?.consignatariaId || item?.consignataria || '', provincia: location?.provinciaNombre || location?.provinciaId || item?.provincia || '', lugar: location?.lugar || item?.lugar || '', localidad: location?.localidad || item?.localidad || '',
16141      fecha: this.rematePayloadDate(schedule?.fecha || item?.fecha || scheduledAt.date), hora: String(schedule?.hora || item?.hora || scheduledAt.time).slice(0, 5), urlStreaming: this.rematePayloadUrl(schedule?.urlStreaming || item?.urlStreaming || ''),
16142      contactoId: contact?.id || item?.contactoId || '', contactoNombre: contact?.name || contact?.nombre || item?.contactName || item?.contactoNombre || '', contactoTelefono: contact?.phone || contact?.telefono || item?.contactPhone || item?.contactoTelefono || '', contactoEmail: contact?.email || item?.contactEmail || item?.contactoEmail || '', contactoNotas: contact?.notes || contact?.notas || item?.contactNotes || item?.contactoNotas || '',
16143      cargaDatos: load?.datos === false || String(item?.cargaDatos || '').toLowerCase() === 'cerrada' ?'cerrada' : 'abierta', cargaLotes: load?.lotes === false || String(item?.cargaLotes || '').toLowerCase() === 'cerrada' ?'cerrada' : 'abierta',
16144      approverUserIds: [...(item?.prebid?.approverUserIds || [])], categoryPricing: (item?.prebid?.categoryPricing || []).map(p => ({...p})),
16145      preInicioFecha: prebidStart.date, preInicioHora: prebidStart.time, preFinFecha: prebidEnd.date, preFinHora: prebidEnd.time,
16146      catalogoPdfName: catalog?.nombreArchivo || '', catalogoPdfSize: catalog?.tamanoBytes || 0, catalogoPdfUrl: catalog?.url || '', catalogoMediaId: catalog?.mediaId || '',
16147      cabanasPrincipales: principals.length ?principals : [this.newCabanaConfigEntry()], cabanasInvitadas: guests.length ?guests : [this.newCabanaConfigEntry()], formulariosCategoria: categories.length ?categories : [this.newRemateCategoryForm()],
16148    };
16149  }
16150  remateTogglesFromItem(item) {
16151    return { requerirRegistro: item?.visualization?.requerirRegistro 
16151=== true, videoFinal: item?.visualization?.habilitarVideoFinal === true, logoRural: item?.visualization?.mostrarLogoCanalRural === true, preHabilitada: item?.prebid?.habilitarPreofertas === true || item?.prebid?.habilitada === true, preIndividual: item?.prebid?.preofertaIndividual === true, apruebaAuto: item?.prebid?.aprobarPreofertasAutomaticamente === true || item?.prebid?.aprobarPrimerasOfertas === true, preAgrupada: item?.prebid?.agruparPreofertas === true, agrupaCorrales: item?.visualization?.agruparCorrales === true };
16152  }
16153  openCabanaRemateEdit(item, updateHash = true) {
16154    this.resetCabanaRematePlaces();
16155    this.setState({ rcSchemaEditor: false });
16156    this.fetchAdminUsers();
16157    if (!this.canEditCabanaRemate(item)) { this.showToast('No tenés permiso para editar este remate.'); return; }
16158    this.fetchProvinces();
16159    const id = this.cabanaRemateId(item);
16160    this.rememberRoute('remateConfig');
16161    this.setState({ route: 'remateConfig', remateConfigForm: this.remateConfigFromItem(item), remateConfigToggles: this.remateTogglesFromItem(item), remateConfigSaved: false, remateConfigEditingId: id, remateConfigStatus: item.status || 'borrador', remateConfigSaving: '' });
16162    if (updateHash) this.syncCabanaLocation(item, 'editar');
16163    window.scrollTo(0, 0);
16164  }
16165  cabanaEligibleApprovers(form = this.state.remateConfigForm) {
16166    const consignatariaId = String(form?.consignataria || '').trim();
16167    if (!consignatariaId) return [];
16168    const self = this.state.user ? {
16169      uid: this.state.user.uid, displayName: this.state.user.displayName, email: this.state.user.email,
16170      roles: this.apiRoleIds(this.state.apiUser),
16171      consignatariaIds: [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(item => item && item.isActive !== false).map(item => String(item.id || item.consignatariaId || '')),
16172    } : null;
16173    return [...(this.state.adminUsers || []), ...(self ? [self] : [])]
16174      .filter((user, index, users) => user?.uid && users.findIndex(candidate => candidate?.uid === user.uid) === index)
16175      .filter(user => (user.roles || []).some(role => ['consignatario', 'certificador', 'cabanero'].includes(role))
16176        && user.consignatariaUserIsActive !== false && user.source?.isActive !== false && !user.source?.disabled
16177        && (user.consignatariaIds || []).map(String).includes(consignatariaId))
16178      .sort((a,b) => String(a.displayName || a.email || '').localeCompare(String(b.displayName || b.email || ''), 'es'));
16179  }
16180  setRemateConfigField(key, value, buffered = false) {
16181    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16182    const next = { ...form, [key]: value };
16183    if (key === 'consignataria' && String(value) !== String(form.consignataria || '')) next.approverUserIds = [];
16184    if (key === 'provincia') {
16185      next.localidad = '';
16186      this.resetCabanaRematePlaces();
16187    }
16188    const patch = { remateConfigForm: next, remateConfigSaved: false };
16189    if (buffered) this.bufferInputState('remate-config-' + key, patch);
16190    else this.setState(patch);
16191  }
16192  cabanaOptionalCategoryPricing(form, categories) {
16193    return (categories || []).filter(category => category.categoryId).flatMap(category => {
16194      const price = (form.categoryPricing || []).find(item => String(item.categoryId) === String(category.categoryId));
16195      const base = String(price?.baseAmount ?? '').trim();
16196      const increment = String(price?.incrementAmount ?? '').trim();
16197      if (!base && !increment) return [];
16198      if (!base || !increment || !Number.isFinite(Number(base)) || Number(base) < 0 || !Number.isFinite(Number(increment)) || Number(increment) <= 0) {
16199        throw new Error('En ' + (category.categoria || 'la categoría') + ', completá una base válida y un incremento mayor a cero, o dejá ambos vacíos para usar los importes de cada lote.');
16200      }
16201      return [{categoryId:category.categoryId,baseAmount:Number(base),incrementAmount:Number(increment)}];
16202    });
16203  }
16204  setCabanaCategoryPrice(categoryId, key, value) {
16205    if (!categoryId || !['baseAmount','incrementAmount'].includes(key)) return;
16206    const form=this.state.remateConfigForm || {};
16207    const prices=form.categoryPricing || [];
16208    const current=prices.find(p=>p.categoryId===categoryId) || {};
16209    this.setRemateConfigField('categoryPricing',[...prices.filter(p=>p.categoryId!==categoryId),{...current,categoryId,[key]:value}],true);
16210  }
16211  addCabanaConfigEntry(group) {
16212    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16213    this.setState({ remateConfigForm: { ...form, [group]: [...(form[group] || []), this.newCabanaConfigEntry()] }, remateConfigSaved: false });
16214  }
16215  setCabanaConfigEntry(group, id, patch) {
16216    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16217    const nextState = { remateConfigForm: { ...form, [group]: (form[group] || []).map((item) => item.id === id ?{ ...item, ...patch } : item) }, remateConfigSaved: false };
16218    if (Object.keys(patch).every((key) => key === 'nombre')) this.bufferInputState('remate-config-text', nextState, 450);
16219    else this.setState(nextState);
16220  }
16221  removeCabanaConfigEntry(group, id) {
16222    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16223    const next = (form[group] || []).filter((item) => item.id !== id);
16224    this.setState({ remateConfigForm: { ...form, [group]: next.length ?next : [this.newCabanaConfigEntry()] }, remateConfigSaved: false });
16225  }
16226  processCabanaLogoDataUrl(dataUrl) {
16227    return new Promise((resolve, reject) => {
16228      const image = new Image();
16229      image.crossOrigin = 'anonymous';
16230      image.onload = () => {
16231        try {
16232          const sampleMax = 360;
16233          const sampleScale = Math.min(1, sampleMax / Math.max(image.naturalWi
16233dth || 1, image.naturalHeight || 1));
16234          const sample = document.createElement('canvas');
16235          sample.width = Math.max(1, Math.round(image.naturalWidth * sampleScale));
16236          sample.height = Math.max(1, Math.round(image.naturalHeight * sampleScale));
16237          const sampleContext = sample.getContext('2d', { willReadFrequently: true });
16238          sampleContext.drawImage(image, 0, 0, sample.width, sample.height);
16239          const pixels = sampleContext.getImageData(0, 0, sample.width, sample.height).data;
16240          const buckets = new Map();
16241          for (let index = 0; index < pixels.length; index += 16) {
16242            const red = pixels[index], green = pixels[index + 1], blue = pixels[index + 2], alpha = pixels[index + 3];
16243            if (alpha < 48 || (red > 242 && green > 242 && blue > 242)) continue;
16244            const max = Math.max(red, green, blue), min = Math.min(red, green, blue);
16245            const saturation = max - min;
16246            const lightness = (max + min) / 2;
16247            if (lightness < 12 || lightness > 246) continue;
16248            const key = (red >> 4) + '-' + (green >> 4) + '-' + (blue >> 4);
16249            const current = buckets.get(key) || { count: 0, red: 0, green: 0, blue: 0, saturation: 0 };
16250            current.count += 1; current.red += red; current.green += green; current.blue += blue; current.saturation += saturation;
16251            buckets.set(key, current);
16252          }
16253          let winner = null, winnerScore = -1;
16254          buckets.forEach((bucket) => {
16255            const averageSaturation = bucket.saturation / Math.max(1, bucket.count);
16256            const score = bucket.count * (1 + averageSaturation / 85);
16257            if (score > winnerScore) { winner = bucket; winnerScore = score; }
16258          });
16259          const channel = (value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, '0');
16260          const brandColor = winner ?'#' + channel(winner.red / winner.count) + channel(winner.green / winner.count) + channel(winner.blue / winner.count) : '#0a5ca8';
16261
16262          const outputMax = 1800;
16263          const outputScale = Math.min(1, outputMax / Math.max(image.naturalWidth || 1, image.naturalHeight || 1));
16264          const whiteCanvas = document.createElement('canvas');
16265          whiteCanvas.width = Math.max(1, Math.round(image.naturalWidth * outputScale));
16266          whiteCanvas.height = Math.max(1, Math.round(image.naturalHeight * outputScale));
16267          const whiteContext = whiteCanvas.getContext('2d', { willReadFrequently: true });
16268          whiteContext.drawImage(image, 0, 0, whiteCanvas.width, whiteCanvas.height);
16269          const whitePixels = whiteContext.getImageData(0, 0, whiteCanvas.width, whiteCanvas.height);
16270          let visibleWeight = 0, visibleLuminance = 0, visibleSaturation = 0;
16271          for (let index = 0; index < whitePixels.data.length; index += 4) {
16272            const alpha = whitePixels.data[index + 3] / 255;
16273            if (alpha < .08) continue;
16274            const red = whitePixels.data[index], green = whitePixels.data[index + 1], blue = whitePixels.data[index + 2];
16275            visibleWeight += alpha;
16276            visibleLuminance += ((red * .2126) + (green * .7152) + (blue * .0722)) * alpha;
16277            visibleSaturation += (Math.max(red, green, blue) - Math.min(red, green, blue)) * alpha;
16278          }
16279          const averageLuminance = visibleWeight ?visibleLuminance / visibleWeight : 0;
16280          const averageSaturation = visibleWeight ?visibleSaturation / visibleWeight : 0;
16281          const sourceIsLightMark = averageLuminance > 205 && averageSaturation < 45;
16282          for (let index = 0; index < whitePixels.data.length; index += 4) {
16283            const sourceAlpha = whitePixels.data[index + 3];
16284            if (!sourceIsLightMark) {
16285              const red = whitePixels.data[index], green = whitePixels.data[index + 1], blue = whitePixels.data[index + 2];
16286              const distanceFromWhite = 255 - Math.min(red, green, blue);
16287              const mask = Math.max(0, Math.min(1, (distanceFromWhite - 10) / 55));
16288              whitePixels.data[index + 3] = Math.round(sourceAlpha * mask);
16289            }
16290            whitePixels.data[index] = 255;
16291            whitePixels.data[index + 1] = 255;
16292            whitePixels.data[index + 2] = 255;
16293          }
16294          whiteContext.clearRect(0, 0, whiteCanvas.width, whiteCanvas.height);
16295          whiteContext.putImageData(whitePixels, 0, 0);
16296          resolve({ logoWhiteDataUrl: whiteCanvas.toDataURL('image/png'), brandColor });
16297        } catch (error) { reject(error); }
16298      };
16299      image.onerror = reject;
16300      image.src = window.resolveCabanaMediaUrl(dataUrl);
16301    });
16302  }
16303  readCabanaConfigLogo(group, id, file) {
16304    if (!file) return;
16305    if (file.type !== 'image/png' && !/\.png$/i.test(file.name || '')) { this.showToast('El logo debe ser un archivo PNG.'); return; }
16306    if (file.size > 2 * 1024 * 1024) { this.showToast('El logo PNG no puede superar 2 MB.'); return; }
16307    const reader = new FileReader();
16308    reader.onload = async () => {
16309      const logoDataUrl = String(reader.result || '');
16310      try {
16311        const generated = await this.processCabanaLogoDataUrl(logoDataUrl);
16312        this.setCabanaConfigEntry(group, id, { logoName: file.name, logoDataUrl, ...generated });
16313      } catch {
16314        this.setCabanaConfigEntry(group, id, { logoName: file.name, logoDataUrl, logoWhiteDataUrl: '', brandColor: '' });
16315        this.showToast('El logo se cargó, pero no se pudo generar su variante blanca.');
16316      }
16317    };
16318    reader.onerror = () => this.showToast('No se pudo leer el logo seleccionado.');
16319    reader.readAsDataURL(file);
16320  }
16321  readRemateCatalog(file) {
16322    if (!file) return;
16323    if (file.type !== 'application/pdf' && !/\.pdf$/i.test(file.name || '')) { this.showToast('El catálogo debe ser un archivo PDF.'); return; }
16324    if (file.size > 30 * 1024 * 1024) { this.showToast('El catálogo PDF no puede superar 30 MB.'); return; }
16325    this._remateCatalogFile = file;
16326    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16327    this.setState({ remateConfigForm: { ...form, catalogoPdfName: file.name, catalogoPdfSize: file.size }, remateConfigSaved: false });
16328  }
16329  removeRemateCatalog() {
16330    this._remateCatalogFile = null;
16331    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16332    this.setState({ remateConfigForm: { ...form, catalogoPdfName: '', catalogoPdfSize: 0, catalogoPdfUrl: '', catalogoMediaId: '' }, remateConfigSaved: false });
16333  }
16334  addRemateCategoryForm() {
16335    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16336    this.setState({ remateConfigForm: { ...form, formulariosCategoria: [...(form.formulariosCategoria || []), this.newRemateCategoryForm()] }, remateConfigSaved: false });
16337  }
16338  setRemateCategoryForm(id, value) {
16339    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16340    this.bufferInputState('remate-config-text', { remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((item) => item.id === id ?{ ...item, categoria: value } : item) }, remateConfigSaved: false }, 450);
16341  }
16342  removeRemateCategoryForm(id) {
16343    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16344    const next = (form.formulariosCategoria || []).filter((item) => item.id !== id);
16345    this.setState({ remateConfigForm: { ...form, formulariosCategoria: next.length ?next : [this.newRemateCategoryForm()] }, remateConfigSaved: false });
16346  }
16347  addRemateSubform(categoryId) {
16348    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16349    this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); return category.id === categoryId ?{ ...category, formularios: [...category.formularios, this.newRemateSubform(category.formularios.length + 1)] } : category; }) }, remateConfigSaved: false });
16350  }
16351  setRemateSubform(categoryId, subformId, patch) {
16352    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16353    const nextState = { remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); return category.id === categoryId ?{ ...category, formularios: category.formularios.map((subform) => subform.id === subformId ?{ ...subform, ...patch } : subform) } : category; }) }, remateConfigSaved: false };
16354    if (Object.keys(patch).every((key) => key === 'nombre')) this.bufferInputState('remate-config-text', nextState, 450);
16355    else this.setState(nextState);
16356  }
16357  startRemateConfigDrag(type, categoryId, subformId, fieldId, event) {
16358    this._remateConfigDrag = { type, categoryId, subformId, fieldId };
16359    if (event?.dataTransfer) {
16360      event.dataTransfer.effectAllowed = 'move';
16361      event.dataTransfer.setData('text/plain', fieldId || subformId || 'remate-item');
16362    }
16363  }
16364  allowRemateConfigDrop(event) {
16365    if (event?.preventDefault) event.preventDefault();
16366    if (event?.dataTransfer) event.dataTransfer.dropEffect = 'move';
16367  }
16368  finishRemateConfigDrag() {
16369    this._remateConfigDrag = null;
16370  }
16371  bindRemateConfigSorting() {
16372    this._onRemateSortPointerDown = (event) => {
16373      const handle = event.target?.closest?.('[aria-label^="Ordenar subformulario"], [aria-label^="Ordenar campo"]');
16374      if (!handle) return;
16375      const [label, categoryId, subformId, fieldId] = (handle.getAttribute('aria-label') || '').split('~');
16376      const kind = label.startsWith('Ordenar campo') ?'field' : 'subform';
16377      this.startRemateConfigDrag(kind, categoryId, subformId, fieldId || null, event);
16378      const element = handle.closest(kind === 'field' ?'[aria-label^="Destino campo"]' : '[aria-label^="Destino subformulario"]');
16379      this._remateSortVisual = element ? {
16380        element,
16381        handle,
16382        startY: event.clientY,
16383        target: null,
16384        original: {
16385          transform: element.style.transform,
16386          transition: element.style.transition,
16387          position: element.style.position,
16388          zIndex: element.style.zIndex,
16389          boxShadow: element.style.boxShadow,
16390          opacity: element.style.opacity,
16391          pointerEvents: element.style.pointerEvents,
16392        },
16393      } : null;
16394      if (element) {
16395        element.style.position = 'relative';
16396        element.style.zIndex = '30';
16397        element.style.boxShadow = '0 16px 34px rgba(0, 35, 70, 0.18)';
16398        element.style.opacity = '0.96';
16399        element.style.pointerEvents = 'none';
16400        element.style.willChange = 'transform';
16401        handle.style.cursor = 'grabbing';
16402      }
16403      document.body.style.userSelect = 'none';
16404      event.preventDefault();
16405    };
16406    this._onRemateSortPointerMove = (event) => {
16407      const drag = this._remateConfigDrag;
16408      const visual = this._remateSortVisual;
16409      if (!drag || !visual) return;
16410      visual.element.style.transform = `translate3d(0, ${event.clientY - visual.startY}px, 0)`;
16411      const target = document.elementFromPoint(event.clientX, event.clientY)?.closest?.('[aria-label^="Destino subformulario"], [aria-label^="Destino campo"]');
16412      if (!target) { event.preventDefault(); return; }
16413      const [label, categoryId, subformId, fieldId] = (target.getAttribute('aria-label') || '').split('~');
16414      const kind = label === 'Destino campo' ?'field' : 'subform';
16415      if (kind === drag.type && categoryId === drag.categoryId && (drag.type !== 'field' || subformId === drag.subformId)) {
16416        visual.target = { kind, categoryId, subformId, fieldId };
16417      }
16418      event.preventDefault();
16419    };
16420    this._onRemateSortPointerUp = () => {
16421      const drag = this._remateConfigDrag;
16422      const visual = this._remateSortVisual;
16423      if (visual?.element) {
16424        Object.assign(visual.element.style, visual.original);
16425        visual.element.style.willChange = '';
16426        if (visual.handle) visual.handle.style.cursor = 'grab';
16427      }
16428      this._remateSortVisual = null;
16429      if (drag && visual?.target) {
16430        if (drag.type === 'subform') this.reorderRemateSubforms(drag.categoryId, visual.target.subformId, null);
16431        if (drag.type === 'field') this.reorderRemateFormFields(drag.categoryId, drag.subformId, visual.target.fieldId, null);
16432      } else {
16433        this.finishRemateConfigDrag();
16434      }
16435      document.body.style.userSelect = '';
16436    };
16437    document.addEventListener('pointerdown', this._onRemateSortPointerDown);
16438    window.addEventListener('pointermove', this._onRemateSortPointerMove, { passive: false });
16439    window.addEventListener('pointerup', this._onRemateSortPointerUp);
16440    window.addEventListener('pointercancel', this._onRemateSortPointerUp);
16441  }
16442  reorderRemateSubforms(categoryId, targetSubformId, event, keepDragging = false) {
16443    if (event?.preventDefault) event.preventDefault();
16444    const drag = this._remateConfigDrag;
16445    if (!keepDragging) this._remateConfigDrag = null;
16446    if (!drag || drag.type !== 'subform' || drag.categoryId !== categoryId || drag.subformId === targetSubformId) return;
16447    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16448    const categories = (form.formulariosCategoria || []).map((rawCategory) => {
16449      const category = this.normalizeRemateCategoryForm(rawCategory);
16450      if (category.id !== categoryId) return category;
16451      const items = [...category.formularios];
16452      const sourceIndex = items.findIndex((item) => item.id === drag.subformId);
16453      const targetIndex = items.findIndex((item) => item.id === targetSubformId);
16454      if (sourceIndex < 0 || targetIndex < 0) return category;
16455      const [moved] = items.splice(sourceIndex, 1);
16456      items.splice(targetIndex, 0, moved);
16457      return { ...category, formularios: items.map((item, index) => ({ ...item, orden: index + 1 })) };
16458    });
16459    this.setState({ remateConfigForm: { ...form, formulariosCategoria: categories }, remateConfigSaved: false });
16460    if (keepDragging) this._remateConfigDrag = drag;
16461  }
16462  removeRemateSubform(categoryId, subformId) {
16463    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16464    this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); if (category.id !== categoryId) return category; const next = category.formularios.filter((subform) => subform.id !== subformId); return { ...category, formularios: next.length ?next : [this.newRemateSubform()] }; }) }, remateConfigSaved: false });
16465  }
16466  addRemateFormField(categoryId, subformId) {
16467    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16468    this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); return category.id === categoryId ?{ ...category, formularios: category.formularios.map((subform) => subform.id === subformId ?{ ...subform, campos: [...(subform.campos || []), this.newRemateFormField((subform.campos || []).length + 1)] } : subform) } : category; }) }, remateConfigSaved: false });
16469  }
16470  setRemateFormField(categoryId, subformId, fieldId, patch) {
16471    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16472    const categories = (form.formulariosCategoria || []).map((category) => this.normalizeRemateCategoryForm(category));
16473    if (patch.mostrarTarjeta === true) {
16474      const category = categories.find((item) => item.id === categoryId);
16475      const visibleCount = (category?.formularios || []).flatMap((subform) => subform.campos || []).filter((field) => field.mostrarTarjeta && field.id !== fieldId).length;
16476      if (visibleCount >= 4) { this.showToast('Podés mostrar hasta 4 campos por categoría en la tarjeta.'); return; }
16477    }
16478    const nextState = { remateConfigForm: { ...form, formulariosCategoria: categories.map((category) => {
16479      if (category.id !== categoryId) return category;
16480      const exclusiveIdentifier = patch.identificadorPrincipal === true;
16481      return { ...category, formularios: category.formularios.map((subform) => ({
16482        ...subform,
16483        campos: (subform.campos || []).map((field) => field.id === fieldId
16484          ?{ ...field, ...patch }
16485          : subform.id === subformId ?{ ...field, ...(exclusiveIdentifier ?{identificadorPrincipal:false}:{}), ...(patch.identificadorCaracteristico === true ?{identificadorCaracteristico:false}:{}) } : field),
16486      })) };
16487    }) }, remateConfigSaved: false };
16488    if (Object.keys(patch).every((key) => ['nombre', 'opciones'].includes(key))) this.bufferInputState('remate-config-text', nextState, 450);
16489    else this.setState(nextState);
16490  }
16491  reorderRemateFormFields(categoryId, subformId, targetFieldId, event, keepDragging = false) {
16492    if (event?.preventDefault) event.preventDefault();
16493    const drag = this._remateConfigDrag;
16494    if (!keepDragging) this._remateConfigDrag = null;
16495    if (!drag || drag.type !== 'field' || drag.categoryId !== categoryId || drag.subformId !== subformId || drag.fieldId === targetFieldId) return;
16496    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16497    const categories = (form.formulariosCategoria || []).map((rawCategory) => {
16498      const category = this.normalizeRemateCategoryForm(rawCategory);
16499      if (category.id !== categoryId) return category;
16500      return { ...category, formularios: category.formularios.map((subform) => {
16501        if (subform.id !== subformId) return subform;
16502        const items = [...(subform.campos || [])];
16503        const sourceIndex = items.findIndex((item) => item.id === drag.fieldId);
16504        const targetIndex = items.findIndex((item) => item.id === targetFieldId);
16505        if (sourceIndex < 0 || targetIndex < 0) return subform;
16506        const [moved] = items.splice(sourceIndex, 1);
16507        items.splice(targetIndex, 0, moved);
16508        return { ...subform, campos: items.map((item, index) => ({ ...item, orden: index + 1 })) };
16509      }) };
16510    });
16511    this.setState({ remateConfigForm: { ...form, formulariosCategoria: categories }, remateConfigSaved: false });
16512    if (keepDragging) this._remateConfigDrag = drag;
16513  }
16514  removeRemateFormField(categoryId, subformId, fieldId) {
16515    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16516    this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); if (category.id !== categoryId) return category; return { ...category, formularios: category.formularios.map((subform) => { if (subform.id !== subformId) return subform; const next = (subform.campos || []).filter((field) => field.id !== fieldId); return { ...subform, campos: next.length ?next : [this.newRemateFormField()] }; }) }; }) }, remateConfigSaved: false });
16517  }
16518  toggleRemateConfig(key) { this.setState({ remateConfigToggles: { ...(this.state.remateConfigToggles || this.emptyRemateConfigToggles()), [key]: !(this.state.remateConfigToggles || this.emptyRemateConfigToggles())[key] }, remateConfigSaved: false }); }
16519  resetRemateConfig() {
16520    this.resetCabanaRematePlaces();
16521    this._remateCatalogFile = null;
16522    const editingId = String(this.state.remateConfigEditingId || '');
16523    const item = editingId ?(this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === editingId) : null;
16524    this.setState({ remateConfigForm: item ?this.remateConfigFromItem(item) : this.emptyRemateConfig('Remate Cabañas'), remateConfigToggles: item ?this.remateTogglesFromItem(item) : this.emptyRemateConfigToggles(), remateConfigSaved: false });
16525  }
16526  rematePayloadKey(value) {
16527    return String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
16528  }
16529  rematePayloadDate(value) {
16530    const raw = String(value || '').trim();
16531    if (!raw) return '';
16532    const iso = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
16533    if (iso) return raw;
16534    const local = raw.match(/^(\d{2})[-/](\d{2})[-/](\d{4})$/);
16535    if (local) return local[3] + '-' + local[2] + '-' + local[1];
16536    const parsed = new Date(raw);
16537    if (Number.isNaN(parsed.getTime())) return raw;
16538    const pad = (number) => String(number).padStart(2, '0');
16539    return parsed.getFullYear() + '-' + pad(parsed.getMonth() + 1) + '-' + pad(parsed.getDate());
16540  }
16541  rematePayloadTime(value) {
16542    const raw = String(value || '').trim();
16543    if (!raw) return '';
16544    const match = raw.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/);
16545    if (!match) return raw;
16546    return String(Number(match[1])).padStart(2, '0') + ':' + match[2] + ':' + (match[3] || '00');
16547  }
16548  rematePayloadUrl(value) {
16549    const raw = String(value || '').trim();
16550    if (!raw) return '';
16551    const markdown = raw.match(/^\[[^\]]*\]\((https?:\/\/[^\s)]+)\)$/i);
16552    return markdown ?markdown[1] : raw;
16553  }
16554  rematePayloadNumber(value) {
16555    if (value === '' || value === null || value === undefined) return null;
16556    const number = Number(String(value).replace(/\./g, '').replace(',', '.'));
16557    return Number.isFinite(number) ?number : null;
16558  }
16559  rematePayloadDateTime(date, time) {
16560    if (!date || !time) return null;
16561    const normalizedTime = /^\d{2}:\d{2}:\d{2}$/.test(String(time)) ?String(time) : String(time).slice(0, 5) + ':00';
16562    const localDate = new Date(String(date) + 'T' + normalizedTime + '-03:00');
16563    return Number.isNaN(localDate.getTime()) ?String(date) + 'T' + normalizedTime : localDate.toISOString();
16564  }
16565  buildCabanaRematePayload(form, toggles, principals, guests, categoryForms, status = 'borrador') {
16566    const fieldTypes = { texto: 'Texto', numero: 'Numero', fecha: 'Fecha', seleccion: 'Seleccion', booleano: 'Booleano' };
16567    const serializeField = (field, fieldIndex) => ({
16568      name: String(field.nombre || '').trim(),
16569      type: fieldTypes[String(field.tipo || 'texto').toLowerCase()] || 'Texto',
16570      required: !!field.requerido,
16571      showOnCard: !!field.mostrarTarjeta,
16572      isPrimaryIdentifier: !!field.identificadorPrincipal,
16573      isCharacteristicIdentifier: !!field.identificadorCaracteristico,
16574      order: fieldIndex + 1,
16575      options: field.tipo === 'seleccion' ?String(field.opciones || '').split(',').map((option) =>
16575 option.trim()).filter(Boolean).map((label) => {
16576        const original = (field.opcionesOriginales || []).find((option) => typeof option === 'object' && (option.label || option.name || option.value) === label);
16577        return { value: original?.value || label, label };
16578      }) : [],
16579    });
16580    const serializeCabana = (item, index) => ({
16581      id: null,
16582      nombre: String(item.nombre || '').trim(),
16583      logoName: String(item.logoName || ''),
16584      logoDataUrl: String(item.logoDataUrl || ''),
16585      displayOrder: index + 1,
16586    });
16587    return {
16588      title: String(form.titulo || '').trim(),
16589      consignatariaId: String(form.consignataria || '').trim(),
16590      cabezasEstimadas: this.rematePayloadNumber(form.cabezas),
16591      status,
16592      location: {
16593        provinciaId: this.rematePayloadKey(form.provincia),
16594        provinciaNombre: String(form.provincia || '').trim(),
16595        localidad: String(form.localidad || '').trim(),
16596        lugar: String(form.lugar || '').trim(),
16597      },
16598      schedule: {
16599        fecha: this.rematePayloadDate(form.fecha),
16600        hora: this.rematePayloadTime(form.hora),
16601        urlStreaming: this.rematePayloadUrl(form.urlStreaming) || null,
16602      },
16603      contacto: {
16604        ...(String(form.contactoId || '').trim() ?{ id: String(form.contactoId).trim() } : {}),
16605        name: String(form.contactoNombre || '').trim() || null,
16606        phone: String(form.contactoTelefono || '').trim() || null,
16607        email: String(form.contactoEmail || '').trim() || null,
16608        notes: String(form.contactoNotas || '').trim() || null,
16609      },
16610      load: {
16611        datos: form.cargaDatos === 'abierta',
16612        lotes: form.cargaLotes === 'abierta',
16613      },
16614      cabanasPrincipales: principals.map(serializeCabana),
16615      cabanasInvitadas: guests.map(serializeCabana),
16616      formsByCategory: categoryForms.map((category) => {
16617        const orderedForms = [...category.formularios].sort((a, b) => Number(a.orden || 0) - Number(b.orden || 0));
16618        return {
16619          categoryId: category.categoryId || null,
16620          categoryName: String(category.categoria || '').trim(),
16621          forms: orderedForms.map((subform, subformIndex) => ({
16622            templateId: subform.templateId || null,
16623            name: String(subform.nombre || '').trim(),
16624            displayOrder: subformIndex + 1,
16625            isActive: subform.activo !== false,
16626            fields: (subform.campos || []).filter((field) => field.nombre).map(serializeField),
16627          })),
16628        };
16629      }),
16630      prebid: {
16631        habilitarPreofertas: !!toggles.preHabilitada,
16632        fechaHoraInicioPreoferta: this.rematePayloadDateTime(form.preInicioFecha, form.preInicioHora),
16633        fechaHoraFinPreoferta: this.rematePayloadDateTime(form.preFinFecha, form.preFinHora),
16634        approverUserIds: [...new Set(form.approverUserIds || [])],
16635        preofertaIndividual: !!toggles.preIndividual,
16636        categoryPricing: this.cabanaOptionalCategoryPricing(form, categoryForms),
16637        aprobarPreofertasAutomaticamente: !!toggles.apruebaAuto,
16638        agruparPreofertas: !!toggles.preAgrupada,
16639      },
16640      visualization: {
16641        requerirRegistro: !!toggles.requerirRegistro,
16642        habilitarVideoFinal: !!toggles.videoFinal,
16643        mostrarLogoCanalRural: !!toggles.logoRural,
16644        agruparCorrales: !!toggles.agrupaCorrales,
16645      },
16646      catalog: form.catalogoPdfName ? {
16647        nombreArchivo: String(form.catalogoPdfName),
16648        tamanoBytes: Number(form.catalogoPdfSize) || 0,
16649        url: String(form.catalogoPdfUrl || '').trim() || null,
16650        mediaId: String(form.catalogoMediaId || '').trim() || null,
16651      } : null,
16652    };
16653  }
16654  remateConfigSchemaSignature(item) {
16655    return JSON.stringify(this.cabanaRemateSchema(item).map((category) => ({
16656      name: String(category.categoryName || '').trim(),
16657      forms: [...(category.forms || [])].sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)).map((form) => ({
16658        name: String(form.name || '').trim(), active: form.isActive !== false,
16659        fields: [...(form.fields || [])].sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map((field) => ({
16660          name: String(field.name || '').trim(), type: String(field.type || '').toLowerCase(),
16661          card: !!field.showOnCard, primary: !!field.isPrimaryIdentifier, characteristic: !!field.isCharacteristicIdentifier,
16662          options: (field.options || []).map((option) => typeof option === 'string' ?{ value: option, label: option } : { value: option.value, label: option.label }),
16663        })),
16664      })),
16665    }
16665)));
16666  }
16667  async saveRemateConfig(status = 'borrador') {
16668    if (!this.state.remateConfigEditingId && !this.canCreateCabanaRemate()) { this.showToast('Solo admin y sysadmin pueden crear remates de cabaña.'); return; }
16669    if (this.state.remateConfigSaving) return;
16670    const form = this.state.remateConfigForm || this.emptyRemateConfig();
16671    const required = [['titulo','título'],['consignataria','consignataria'],['fecha','fecha'],['hora','hora']];
16672    const missing = required.filter(([key]) => !String(form[key] || '').trim()).map(([, label]) => label);
16673    if (missing.length) { this.showToast('Completá ' + missing.join(', ') + '.'); return; }
16674    const scheduleDate = this.rematePayloadDate(form.fecha);
16675    const scheduleTime = this.rematePayloadTime(form.hora);
16676    const scheduledAt = new Date(scheduleDate + 'T' + scheduleTime);
16677    if (Number.isNaN(scheduledAt.getTime())) { this.showToast('Ingresá una fecha y hora válidas.'); return; }
16678    if (status === 'abierto' && !this.state.rcSchemaEditor && scheduledAt.getTime() < Date.now()) { this.showToast('La fecha y hora del remate deben ser futuras para publicarlo como abierto.'); return; }
16679    const streamingUrl = this.rematePayloadUrl(form.urlStreaming);
16680    if (streamingUrl) {
16681      try {
16682        const parsedUrl = new URL(streamingUrl);
16683        if (!['http:', 'https:'].includes(parsedUrl.protocol)) throw new Error('invalid_protocol');
16684      } catch { this.showToast('Ingresá una URL de streaming válida, por ejemplo https://...'); return; }
16685    }
16686    if (form.contactoEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(form.contactoEmail).trim())) { this.showToast('Ingresá un email de contacto válido.'); return; }
16687    if (String(form.contactoTelefono || '').trim() && String(form.contactoTelefono).replace(/\D/g, '').length < 8) { this.showToast('Ingresá un teléfono de contacto válido.'); return; }
16688    const principals = (form.cabanasPrincipales || []).filter((item) => item.nombre || item.logoDataUrl);
16689    const guests = (form.cabanasInvitadas || []).filter((item) => item.nombre || item.logoDataUrl);
16690    if (!principals.length) { this.showToast('Agregá al menos una cabaña principal.'); return; }
16691    if ([...principals, ...guests].some((item) => {
16692      const logo = String(item.logoDataUrl || '').trim();
16693      return !String(item.nombre || '').trim() || (logo && !/^data:image\/png;base64,/i.test(logo) && !/^https?:\/\//i.test(logo));
16694    })) { this.showToast('Cada cabaña debe tener nombre. Si cargás un logo, debe ser un PNG válido.'); return; }
16695    const categoryForms = (this.state.remateConfigEditingId ?(form.formulariosCategoria || []) : []).map((category) => this.normalizeRemateCategoryForm(category)).filter((category) => category.categoria || category.formularios.some((subform) => (subform.campos || []).some((field) => field.nombre)));
16696    if (categoryForms.some((category) => !category.categoria || !category.formularios.length || category.formularios.some((subform) => !subform.nombre || !(subform.campos || []).some((field) => field.nombre)))) { this.showToast('Cada categoría y subformulario debe tener nombre y al menos un campo.'); return; }
16697    if (categoryForms.some((category) => category.formularios.some((subform) => (subform.campos || []).some((field) => field.nombre && field.tipo === 'seleccion' && !String(field.opciones || '').trim())))) { this.showToast('Completá las opciones de los campos de selección.'); return; }
16698    if (categoryForms.some((category) => category.formularios.flatMap((subform) => subform.campos || []).filter((field) => field.mostrarTarjeta).length > 4)) { this.showToast('Solo se permiten 4 campos visibles por categoría.'); return; }
16699    if (categoryForms.some((category) => category.formularios.some((subform) => (subform.campos || []).filter((field) => field.identificadorPrincipal).length > 1))) { this.showToast('Solo un campo por formulario puede ser el identificador principal.'); return; }
16700    const toggles = this.state.remateConfigToggles || this.emptyRemateConfigToggles();
16701    if (categoryForms.some(c => c.formularios.some(f => (f.campos || []).filter(x => x.identificadorCaracteristico).length > 1))) { this.showToast('Solo un identificador característico por formulario.'); return; }
16702    try { this.cabanaOptionalCategoryPricing(form, categoryForms); } catch (error) { this.showToast(error.message); return; }
16703    const payload = this.buildCabanaRematePayload(form, toggles, principals, guests, categoryForms, status);
16704    const editingId = String(this.state.remateConfigEditingId || '');
16705    const currentUid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || '');
16706    const currentEmail = String(this.state.apiUser?.email || this.state.user?.email || '').trim().toLowerCase();
16707    this.setState({ remateConfigSaving: status === 'abierto' ?'publish' : 'draft', remateConfigSaved: false });
16708    try {
16709      const path = '/api/cabin-auctions' + (editingId ?'/' + encodeURIComponent(editingId) : '');
16710      const response = await this.cabanaApiFetch(path, { method: editingId ?'PUT' : 'POST', body: JSON.stringify(payload) });
16711      const returned = response?.data || response?.item || response?.auction || response?.cabinAuction || response || {};
16712      const returnedId = this.cabanaRemateId(returned) || editingId;
16713      const saved = { ...payload, ...returned, id: returnedId || returned?.id, creatorUid: returned?.creatorUid || returned?.createdByUid || currentUid, creatorEmail: returned?.creatorEmail || returned?.createdByEmail || currentEmail };
16714      if (Array.isArray(returned.forms) && !Array.isArray(returned.formsByCategory)) delete saved.formsByCategory;
16715      const savedId = this.cabanaRemateId(saved);
16716      if (savedId) {
16717        this.setState({ remateConfigEditingId: savedId });
16718        const confirmation = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(savedId));
16719        const persisted = confirmation?.data || confirmation?.item || confirmation?.auction || confirmation?.cabinAuction || confirmation;
16720        if (!!persisted?.visualization?.requerirRegistro !== !!payload.visualization.requerirRegistro) throw new Error('El servidor no confirmó el acceso bajo registro. Tus cambios siguen en el editor.');
16721        Object.assign(saved,persisted);
16722        if (Array.isArray(persisted?.forms) && !Array.isArray(persisted?.formsByCategory)) delete saved.formsByCategory;
16723        if (this.remateConfigSchemaSignature(persisted) !== this.remateConfigSchemaSignature(payload)) {
16724          throw new Error('El servidor no conservó todos los cambios del formulario. Tus cambios siguen en el editor; no se confirmó el guardado.');
16725        }
16726      } else {
16727        throw new Error('El servidor no devolvió el remate guardado. No se pudo confirmar el guardado.');
16728      }
16729      this.setState({ remateConfigForm: this.remateConfigFromItem(saved), remateConfigSaving: '', remateConfigSaved: true, remateConfigStatus: status, remateConfigEditingId: savedId, cabanas: [saved, ...(this.state.cabanas || []).filter((item) => this.cabanaRemateId(item) !== savedId)] });
16730      if (savedId) this.syncCabanaLocation(saved, 'editar', { replace: true });
16731      this.showToast(this.state.rcSchemaEditor ?'Formulario de lotes guardado.' : status === 'abierto' ?'Remate publicado.' : 'Borrador guardado.');
16732      if (status === 'abierto' && !this.state.rcSchemaEditor) {
16733        await this.fetchCabanas(true);
16734        this.go('cabanas');
16735      }
16736    } catch (error) {
16737      this.setState({ remateConfigSaving: '' });
16738      this.showToast(error?.message || 'No se pudo guardar el remate.');
16739    }
16740  }
16741  scheduleRouteWork(route) {
16742    if (this._routeWorkTimer) clearTimeout(this._routeWorkTimer);
16743    const run = () => {
16744      this._routeWorkTimer = null;
16745      if (this.state.route !== route) return;
16746      if (route === 'home') this.animateStats();
16747      if (route === 'pool') {
16748        this.syncAuctionStatuses();
16749        this.syncLiveBids();
16750        this.fetchNotifications(true);
16751        this.fetchNotificationHistory(true);
16752        if (!this.state.myBids || (this.canManageSellerData(this.state.apiUser) && !this.state.myLots)) this.refreshProfileData();
16753      }
16754      if (route === 'roslotSummary') this.fetchRoslotDailyReport();
16755      if (route === 'profile') this.refreshProfileData();
16756      if (route === 'mailingAdmin') this.fetchMailing();
16757      if (route === 'notifications') this.fetchNotificationHistory();
16758      if (route === 'remateConfig') this.fetchProvinces();
16759      if (route === 'load') {
16760        if (!this.state.comitentes) this.fetchComitentes();
16761        if (!this.state.establishments) this.fetchEstablishments();
16762      }
16763      if (route === 'rosganLotLoad') this.loadRosganLotCatalogs();
16764      if (route === 'remates') {
16765        this.loadRosganRemateAdmin();
16766        if (this.state.monthRemates === null) this.fetchMonthRemates();
16767        this.fetchLatestFinishedRemates();
16768        if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas();
16769        if (this.canManageSellerData(this.state.apiUser)) this.fetchProvinces();
16770      }
16771      if (route === 'rosganRemateCreate' && this.canManageUsers(this.state.apiUser)) {
16772        this.loadRosganRemateAdmin();
16773        this.fetchProvinces();
16774        if (!this.state.rosganRemateFormOpen) this.openRosganRemateForm(null, { navigate: false });
16775      }
16776      if (route === 'cabanas') {
16777        if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas(true);
16778        else if (this.state.cabanasCollectionReady) this.fetchCabanaCardMedia();
16779      }
16780      if (route === 'feria') { this.fetchProvinces(); this.prepareFeriaConsignataria(); }
16781      // The statistics component loads its validated server snapshot and cached MAV data.
16782    };
16783    requestAnimationFrame(() => { this._routeWorkTimer = setTimeout(run, 0); });
16784  }
16785  go(route, options = {}) {
16786    if (route !== 'rosganAuditor' && this._auditorPanel) { this._auditorPanel.close(); this._auditorPanel = null; }
16787    if (!this.state.user && !['home', 'remates', 'cabanas'].includes(route)) {
16788      this.syncAppLocation(route);
16789      this.setState({ route: 'login', routeLoading: false, apiUser: null, authReady: true });
16790      return;
16791    }
16792    this._pathRequest = (this._pathRequest || 0) + 1;
16793    if (this.state.routeLoading) this.setState({ routeLoading: false });
16794    if (route === 'mercado' && !this.canAccessStatistics()) {
16795      this.showToast('Estadísticas está en desarrollo y solo está disponible para admin y sysadmin.');
16796      this.go('home', options);
16797      return;
16798    }
16799    if (['pool', 'lot', 'load', 'roslotSummary'].includes(route) && !this.canAccessRoslot()) {
16800      this.showToast('Por ahora Roslot está disponible solo para administradores.');
16801      if (options.syncPath !== false) this.syncAppLocation('home', { replace: true });
16802      return;
16803    }
16804    if (route === 'feriaConfig' && !this.canManageUsers()) {
16805      this.showToast('El control de dispositivos está disponible sólo para admin y sysadmin.');
16806      this.syncAppLocation('home', { replace: true });
16806 this.go('home', { syncPath: false }); return;
16807    }
16808    if (route === 'feria' && !this.canAccessFeria()) {
16809      this.showToast('Feria está disponible para consignatarios, certificadores y administradores.');
16810      if (options.syncPath !== false) this.syncAppLocation('home', { replace: true });
16811      return;
16812    }
16813    if (route === 'load' && this.state.apiUser && !this.canManageSellerData(this.state.apiUser)) { this.showToast('Tu cuenta no tiene permiso para cargar lotes.'); return; }
16814    if (route === 'rosganLotLoad' && !this.canManageRosganLots(this.state.apiUser)) { this.showToast('Tu cuenta no tiene permiso para cargar lotes Rosgan.'); return; }
16815    if (route === 'rosganRemateCreate' && this.state.apiUser && !this.canManageUsers(this.state.apiUser)) { this.showToast('Solo admin y sysadmin pueden crear remates Rosgan.'); return; }
16816    if (route === 'mailingAdmin' && !this.canManageUsers()) { this.showToast('No tenés permiso para administrar el mailing.'); this.syncAppLocation('profile', { replace: true }); this.go('profile', { syncPath: false }); return; }
16817    if (route === 'remateConfig') { this.openRemateConfig(); return; }
16818    const allowed = ['home', 'pool', 'roslotSummary', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'remateDetail', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'cabanaTvLower', 'cabanaTvConfig', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaSalesOrder', 'channel', 'mercado'];
16819    if (!allowed.includes(route)) { this.showToast('Próxima etapa: ' + route); return; }
16820    if (route === this.state.route) {
16821      if (this.state.filtersOpen) this.setState({ filtersOpen: false });
16822      if (route === 'cabanas') {
16823        if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas(true);
16824        else if (this.state.cabanasCollectionReady) this.fetchCabanaCardMedia();
16825      }
16826      window.scrollTo({ top: 0, behavior: 'auto' });
16827      return;
16828    }
16829    if (options.syncPath !== false && this.appPathForRoute(route)) this.syncAppLocation(route);
16830    this.rememberRoute(route);
16831    if (route !== 'mailingAdmin') document.documentElement.classList.remove('mailing-editor-is-expanded');
16832    this.setState({
16833      route,
16834      filtersOpen: false,
16835      roslotMobilePanelOpen: false,
16836      mailingOpen: route === 'mailingAdmin',
16837      mailingEditorExpanded: route === 'mailingAdmin' ?this.state.mailingEditorExpanded : false,
16838      mailingInsertDialog: route === 'mailingAdmin' ?this.state.mailingInsertDialog : '',
16839    }, () => {
16840      window.scrollTo({ top: 0, behavior: 'auto' });
16841      this.scheduleRouteWork(route);
16842    });
16843  }
16844  async loadRemateListCatalogs(items, kind = 'active') {
16845    await Promise.all((items || []).map(async item => {
16846      const id = kind === 'cabana' ? this.cabanaRemateId(item) : this.remateRouteId(item);
16847      if (!id) return;
16848      const key = kind + ':' + id;
16849      let href = '';
16850      try {
16851        if (kind === 'cabana') {
16852          // Cabañas does not yet support published catalogues.
16853          href = '';
16854        } else {
16855          const suffix = kind === 'historical' ? '&historico=1' : '';
16856          const result = await this.apiFetch('/api/catalog-publication/' + encodeURIComponent(id) + (suffix ? '?historico=1' : ''), { cache: 'no-store' });
16857          if (result.published === true) href = '/catalogo.html?remate=' + encodeURIComponent(id) + suffix;
16858        }
16859      } catch { /* Unconfirmed availability stays hidden. */ }
16860      this.setState({ remateListCatalogs: { ...this.state.remateListCatalogs, [key]: href } });
16861    }));
16862  }
16863  remateListCatalog(item, kind = 'active') {
16864    const id = kind === 'cabana' ? this.cabanaRemateId(item) : this.remateRouteId(item);
16865    const href = kind === 'cabana' ? '' : this.state.remateListCatalogs?.[kind + ':' + id] || '';
16866    return { catalogAvailable: !!href, openCatalog: event => { event?.stopPropagation?.(); if (href) window.open(href, '_blank', 'noopener,noreferrer'); } };
16867  }
16868  catalogPublicationKey(remate = this.state.currentRemate) {
16869    return (remate?.__rosganActive ? '' : 'historico-') + this.remateRouteId(remate);
16870  }
16871  catalogPublicationPath(remate = this.state.currentRemate) {
16872    return '/api/catalog-publication/' + encodeURIComponent(this.remateRouteId(remate)) + (remate?.__rosganActive ? '' : '?historico=1');
16873  }
16874  async fetchCatalogPublication(remate) {
16875    const key = this.catalogPublicationKey(remate);
16876    this.setState({ catalogPublication: { key, published: false, loaded: false, busy: false } });
16877    try {
16878      const result = await this.apiFetch(this.catalogPublicationPath(remate), { cache: 'no-store' });
16879      if (this.catalogPublicationKey() === key && !this.state.catalogPublication?.busy) this.setState({ catalogPublication: { key, published: result.published === true, salesOrderDefined: result.salesOrderDefined === true, loaded: true, busy: false } });
16880    } catch { if (this.catalogPublicationKey() === key) this.showToast('No se pudo consultar la publicación del catálogo.'); }
16881  }
16882  async toggleCatalogPublication() {
16883    const remate = this.state.currentRemate, key = this.catalogPublicationKey(remate), state = this.state.catalogPublication;
16884    if (!this.canManageUsers() || state?.key !== key || !state.loaded || state.busy) return;
16885    this.setState({ catalogPublication: { ...state, busy: true } });
16886    try {
16887      const result = await this.apiFetch(this.catalogPublicationPath(remate), { method: 'PATCH', body: JSON.stringify({ published: !state.published }) });
16888      if (this.catalogPublicationKey() === key) this.setState({ catalogPublication: { key, published: result.published === true, salesOrderDefined: result.salesOrderDefined === true, loaded: true, busy: false } });
16889      this.showToast(result.published ? 'Catálogo publicado.' : 'Catálogo despublicado.');
16890    } catch (error) {
16891      if (this.catalogPublicationKey() === key) this.setState({ catalogPublication: { ...state, busy: false } });
16892      this.showToast(error.message || 'No se pudo guardar la publicación.');
16893    }
16894  }
16895  openRemate(item, options = {}) {
16896    if (item?.__feriaAuction) { window.open(this.histUrl(item), '_blank', 'noopener,noreferrer'); return; }
16897    if (options.syncPath !== false) this.syncRemateLocation(item);
16898    this.setState({ route: 'remateDetail', currentRemate: item, rdQuery: '', rdSort: 'orden', rdView: 'grid', rdLightbox: null, remateLots: null });
16899    window.scrollTo(0, 0);
16900    const loading = this.fetchRemateLots(item, options.lotId || '');
16901    this.fetchCatalogPublication(item);
16902    return loading;
16903  }
16904  normalizeRosganActiveRemate(item = {}) {
16905    const rawDate = item.fechaHoraRemate || item.fecha_hora_remate || item.fecha || '';
16906    const date = rawDate ?new Date(rawDate) : null;
16907    const time = date && !Number.isNaN(date.getTime()) ?date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false }) : '';
16908    return {
16909      ...item,
16910      __rosganActive: true,
16911      id_remate: item.id_remate || item.id,
16912      nombre_remate: item.nombre_remate || item.titulo || 'Remate Rosgan',
16913      nombre_cuenta: item.nombre_cuenta || item.consignatariaNombre || 'Rosgan',
16914      fechaHoraRemate: rawDate,
16915      hora_remate: item.hora_remate || time,
16916      ubicacion_remate: item.ubicacion_remate || [item.localidad, item.provincia].filter(Boolean).join(', ') || item.lugar || '',
16917    };
16918  }
16919  rosganActiveLotThumbnail(source = {}) {
16920    const current = source.finalVideo || {};
16921    const url = source.videoUrl || source.video_uri || current.url || '';
16922    const proof = source.__verifiedThumbnail;
16923    if (!url || !proof || proof.sourceUrl !== url || proof.videoId !== current.id || proof.fileKey !== (current.fileKey || '') || Number(proof.sizeBytes) !== Number(current.sizeBytes) || proof.sha256 !== (current.sha256 || '')) return '';
16924    return typeof proof.thumbnailUrl === 'string' && proof.thumbnailUrl.startsWith('https://') ? proof.thumbnailUrl : '';
16925  }
16926
16927  isRosganLotPublished(lot) {
16928    return String(lot?.status ?? lot?.data?.status ?? lot?.lot?.status ?? '').trim().toLowerCase() === 'publicado';
16929  }
16930  publicRemateLots(lots, remate = this.state.currentRemate) {
16931    const rows = Array.isArray(lots) ? lots : [];
16932    return remate?.__rosganActive ? rows.filter(lot => this.isRosganLotPublished(lot)) : rows;
16933  }
16934  publicRemateHeads(lots, remate = this.state.currentRemate) {
16935    return this.rosganRemateHeads({ ...remate, cabezas: 0 }, this.publicRemateLots(lots, remate));
16936  }
16937  rosganLotWorkflowStatus(lot) {
16938    const value = String(lot?.status ?? lot?.data?.status ?? lot?.lot?.status ?? '').trim().toLowerCase();
16939    return ['cargado', 'controlado', 'publicado', 'anulado', 'cerrado'].includes(value) ?value : (value || 'sin-estado');
16940  }
16941  rosganLotIsAssigned(lot) {
16942    if (this.canManageUsers(this.state.apiUser)) return true;
16943    const id = String(lot?.consignatariaId ?? lot?.data?.consignatariaId ?? lot?.lot?.consignatariaId ?? '');
16944    return !!id && (this.state.userConsignatarias || []).some((member) => String(member.id) === id);
16945  }
16946  canEditRosganLot(lot) {
16947    return this.canManageRosganLots(this.state.apiUser) && this.rosganLotIsAssigned(lot) && this.rosganLotWorkflowStatus(lot) === 'cargado';
16948  }
16949  normalizeRosganActiveLot(item = {}, index = 0, remate = this.state.currentRemate) {
16950    const nested = item.data || item.lot || {};
16951    const source = { ...nested, ...item };
16952    const cert = source.cert || source.lotCert || source.certificate || source.nroCertificado || source.nro_certificado || source.id || '';
16953    const male = Number(source.maleQuantity ?? source.cantidadMachos ?? 0) || 0;
16954    const female = Number(source.femaleQuantity ?? source.cantidadHembras ?? 0) || 0;
16955    const mothers = Number(source.motherQuantity ?? source.cantidadMadres ?? 0) || 0;
16956    const calves = Number(source.calfQuantity ?? source.cantidadCrias ?? 0) || 0;
16957    const quantity = Number(source.totalQuantity ?? source.quantity ?? source.cantidad ?? (male + female || mothers + calves)) || 0;
16958    const category = source.displayCategoryName || source.categoryName || source.categoriaNombre || source.nombreCategoria || source.category?.name || source.categoria?.nombre || source.categoryId || '';
16959    const breedList = Array.isArray(source.breedNames) ?source.breedNames : Array.isArray(source.breeds) ?source.breeds.map((breed) => breed?.name || breed?.nombre || breed?.label || breed) : [];
16960    const breed = breedList.filter(Boolean).join(' · ') || source.raza || source.breedName || '';
16961    const averageWeight = Number(source.averageWeight ?? source.pesoPromedio ?? source.peso_promedio ?? source.invernada?.averageWeight ?? source.invernada?.pesoPromedio ?? source.faena?.averageWeight ?? 0) || 0;
16962    const minimumWeight = Number(source.minimumWeight ?? source.pesoMinimo ?? source.peso_minimo ?? source.invernada?.minWeight ?? source.faena?.minWeight ?? 0) || 0;
16963    const maximumWeight = Number(source.maximumWeight ?? source.pesoMaximo ?? source.peso_maximo ?? source.invernada?.maxWeight ?? source.faena?.maxWeight ?? 0) || 0;
16964    const lotNumber = source.lotNumber ?? source.numeroLote ?? source.numero_lote ?? '';
16965    const order = source.saleOrder ?? source.order ?? source.orden_venta ?? source.orden_final ?? source.displayOrder ?? source.orden ?? '';
16966    const participant = (remate?.consignatariasParticipantes || []).find((entry) => String(entry?.consignatariaId || entry?.id || '') === String(source.consignatariaId || ''));
16967    const consignataria = source.consignatariaNombre || source.nombre_consignatario || participant?.consignatariaNombre || participant?.nombre || source.consignatariaId || remate?.consignatariaNombre || 'Rosgan';
16968    const age = source.ageValue ?[source.ageValue, source.ageUnit].filter(Boolean).join(' ') : (source.edad || '');
16969    const lotDescriptions = Array.isArray(source.lotDescriptions) ?source.lotDescriptions.filter(Boolean).join(' · ') : (source.lotDescriptions || source.descripcion_lote || '');
16970    const weighing = source.invernada?.weighing || source.faena?.weighing || source.pesaje || '';
16971    const desbaste = source.invernada?.desbaste ?? source.faena?.desbaste ?? source.desbaste ?? '';
16972    return {
16973      ...source,
16974      __rosganActiveLot: true,
16975      cert,
16976      nro_certificado: cert,
16977      id_certificado: cert,
16978      lotNumber: lotNumber === '' ?'' : (Number(lotNumber) || lotNumber),
16979      saleOrder: order === '' ?'' : (Number(order) || order),
16980      orden_final: order === '' ?'' : (Number(order) || order),
16981      orden_venta: order === '' ?'' : (Number(order) || order),
16982      cantidad: quantity,
16983      cantidad_machos: male,
16984      cantidad_hembras: female,
16985      cantidad_madres: mothers,
16986      cantidad_crias: calves,
16987      nombre_categoria: category,
16988      raza: breed,
16989      peso_promedio: averageWeight,
16990      peso_minimo: minimumWeight,
16991      peso_maximo: maximumWeight,
16992      plazo: source.paymentTerm || source.plazo || '',
16993      nombre_localidad: source.locality || source.nombre_localidad || source.localidad || '',
16994      nombre_provincia: source.province || source.nombre_provincia || source.provincia || '',
16995      nombre_consignatario: consignataria,
16996      clase: source.animalClass || source.clase || '',
16997      edad: age,
16998      estado: source.bodyCondition || source.estado || '',
16999      destino: source.destination || source.destino || '',
17000      pastoreo: source.nutrition || source.pastoreo || '',
17001      descripcion_lote: lotDescriptions,
17002      denticion: source.dentition || source.denticion || '',
17003      estado_reproductivo: source.reproductiveStatus || source.estado_reproductivo || '',
17004      sanidad: source.health || source.sanidad || '',
17005      garrapata: /^no$/i.test(String(source.tickStatus || source.garrapata || '').trim()) ? '' : (source.tickStatus || source.garrapata || ''),
17006      pesaje: weighing,
17007      desbaste: desbaste === '' || desbaste === null ?'' : String(desbaste) + (String(desbaste).includes('%') ?'' : '%'),
17008      servicio: source.vientres?.service || source.servicio || '',
17009      prenez: source.vientres?.pregnancy || source.prenez || '',
17010      paricion: source.vientres?.calving || source.paricion || '',
17011      faena_subcategoria: source.faena?.subCategory || '',
17012      faena_condicion: source.faena?.conditionDisplayName || source.faena?.conditionType || '',
17013      faena_entrega: source.faena?.deliveryType || '',
17014      faena_terminacion: source.faena?.finishing || '',
17015      faena_rinde: source.faena?.estimatedYield || '',
17016      videoUrl: source.videoUrl || source.video_uri || source.finalVideo?.url || '',
17017      video_uri: source.video_uri || source.videoUrl || source.finalVideo?.url || '',
17018      thumbnailUrl: this.rosganActiveLotThumbnail(source),
17019    };
17020  }
17021  enrichRosganActiveLotMetadata(item = {}, metadata = {}) {
17022    const nested = item.data || item.lot || {};
17023    const source = { ...nested, ...item };
17024    const cert = String(source.cert || source.lotCert || source.certificate || source.nroCertificado || source.nro_certificado || source.id || '');
17025    const stored = metadata?.[cert] && typeof metadata[cert] === 'object' ?metadata[cert] : {};
17026    const first = (...values) => values.find((value) => value !== undefined && value !== null && String(value).trim() !== '') || '';
17027    return {
17028      ...item,
17029      locality: source.locationFromEstablishment ? source.locality : first(source.establishment?.locality, source.locality, source.nombre_localidad, source.localidad, stored.locality),
17030      province: source.locationFromEstablishment ? source.province : first(source.establishment?.province, source.province, source.nombre_provincia, source.provincia, stored.province),
17031      paymentTerm: first(source.paymentTerm, source.plazo, source.commercial?.paymentTerm, source.sale?.paymentTerm, stored.paymentTerm),
17032    };
17033  }
17034  openRosganActiveRemate(item, options = {}) {
17035    const remate = this.normalizeRosganActiveRemate(item);
17036    if (options.syncPath !== false) this.syncRemateLocation(remate);
17037    this.setState({ route: 'remateDetail', currentRemate: remate, rdQuery: '', rdSort: 'orden', rdView: 'grid', rdLightbox: null, remateLots: null, remateLotAlerts: {}, remateLotAlertBusy: '' });
17038    window.scrollTo(0, 0);
17039    const loading = this.fetchRosganActiveLots(remate, options.lotId || '');
17040    this.fetchCatalogPublication(remate);
17041    return loading;
17042  }
17043  async openRosganActiveRemateById(remateId, lotId = '') {
17044    const requestedPath = window.location.pathname;
17045    try {
17046      const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId));
17047      if (window.location.pathname !== requestedPath) return;
17048      const item = response?.data ?? response;
17049      if (!item || item?.cabanasConfig) throw new Error('No es un remate Rosgan activo.');
17050      return this.openRosganActiveRemate(item, { lotId, syncPath: false });
17051    } catch (error) {
17052      if (window.location.pathname !== requestedPath) return;
17053      if (Number(error?.status) !== 404) {
17054        this.setState({ route: 'remateDetail', currentRemate: { id_remate: remateId, __rosganActive: true }, remateLots: [], remateLotsLoading: false, remateLotsReal: false, rdLightbox: null });
17055        this.showToast(error?.message || 'No se pudo cargar el remate desde el API actual.');
17056        return;
17057      }
17058      const historical = (this.state.remates || []).find((candidate) => this.remateRouteId(candidate) === String(remateId)) || { id_remate: remateId };
17059      return this.openRemate(historical, { lotId, syncPath: false });
17060    }
17061  }
17062  async fetchRosganActiveLots(item, lotId = '') {
17063    const remateId = this.remateRouteId(item);
17064    const requestedPath = window.location.pathname;
17065    const request = {};
17066    this._activeLotsRequest = request;
17067    const isCurrent = () => this._activeLotsRequest === request && window.location.pathname === requestedPath;
17068    this.setState({ remateLotsLoading: true, remateLotsReal: false });
17069    try {
17070      let thumbnailManifest = {}, lotMetadata = {}, loaded = false;
17071      const thumbnailCacheKey = 'rosgan:verified-thumbnails:v1:' + remateId;
17072      try {
17073        const cached = JSON.parse(sessionStorage.getItem(thumbnailCacheKey) || 'null');
17074        if (cached && Date.now() - cached.at < 3600000) thumbnailManifest = cached.manifest || {};
17075      } catch {}
17076      const enrich = (lot, index) => {
17077        const source = lot?.data || lot?.lot || lot || {};
17078        const cert = source.cert || source.lotCert || source.certificate || source.nroCertificado || source.nro_certificado || source.id || '';
17079        const proof = thumbnailManifest?.[String(cert)];
17080        const enriched = this.enrichRosganActiveLotMetadata({ ...lot, __verifiedThumbnail: proof && typeof proof === 'object' ? proof : null, thumbnailUrl: '', thumbnail: '', imagen: '' }, lotMetadata);
17081        return this.normalizeRosganActiveLot(enriched, index, item);
17082      };
17083      const refreshExtras = () => {
17084        if (!loaded || !isCurrent()) return;
17085        this.setState((previous) => ({
17086          remateLots: (previous.remateLots || []).map(enrich),
17087          rdLightbox: previous.rdLightbox ? enrich(previous.rdLightbox, 0) : null,
17088        }));
17089      };
17090      // Optional assets must not keep the catalog behind the loading screen.
17091      fetch('/api/remates/' + encodeURIComponent(remateId) + '/video-thumbnails', { cache: 'no-store' })
17092        .then(result => result.ok ? result.json() : {})
17093        .then(value => {
17094          thumbnailManifest = value || {};
17095          try { sessionStorage.setItem(thumbnailCacheKey, JSON.stringify({ at: Date.now(), manifest: thumbnailManifest })); } catch {}
17096          refreshExtras();
17097        }).catch(() => {});
17098      fetch('/assets/remates/' + encodeURIComponent(remateId) + '/lot-metadata.json', { cache: 'no-store' })
17099        .then(result => result.ok ? result.json() : {})
17100        .then(value => { lotMetadata = value || {}; refreshExtras(); }).catch(() => {});
17101      const response = await this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots');
17102      const lots = this.publicRemateLots(this.rosganArray(response), item).map(enrich);
17103      if (!isCurrent()) return;
17104      loaded = true;
17105      const targetLot = lotId ?lots.find((candidate) => this.remateLotRouteId(candidate) === String(lotId)) || null : null;
17106      this.setState({ remateLots: lots, currentRemate: this.normalizeRosganActiveRemate(item), rdLightbox: targetLot, remateLotsReal: true, remateLotsLoading: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true }, () => {
17107        if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo();
17108        if (this.state.user) this.loadRemateLotAlerts(remateId);
17109        if (targetLot) { this.fetchRosganLotVideos(remateId, this.remateLotRouteId(targetLot)); this.fetchRosganRepresentative(targetLot); }
17110      });
17111    } catch (error) {
17112      if (!isCurrent()) return;
17113      this.setState({ remateLots: [], remateLotsReal: false, remateLotsLoading: false });
17114      this.showToast(error?.message || 'No se pudieron cargar los lotes del remate.');
17115    }
17116  }
17117  rosganOpsStorageKey(remate = this.state.currentRemate) {
17118    return 'rosgan:remate-operations:' + (this.remateRouteId(remate) || 'unknown');
17119  }
17120  normalizeRosganSalesOrderSchedule(value) {
17121    const source = value && typeof value === 'object' && !Array.isArray(value) ?value : {};
17122    const seen = new Set();
17123    const entries = (Array.isArray(source.entries) ?source.entries : []).slice(0, 1500).flatMap((raw) => {
17124      const cert = String(raw?.cert ?? '').trim().slice(0, 100);
17125      const alias = String(raw?.alias ?? '').trim().slice(0, 160);
17126      const averageSeconds = Number(raw?.averageSeconds);
17127      const estimatedStartSeconds = Number(raw?.estimatedStartSeconds);
17128      if (!cert || !alias || seen.has(cert) || !Number.isInteger(averageSeconds) || averageSeconds < 1 || averageSeconds > 86400 || !Number.isInteger(estimatedStartSeconds) || estimatedStartSeconds < 0 || estimatedStartSeconds > 604800) return [];
17129      seen.add(cert);
17130      return [{ cert, alias, averageSeconds, estimatedStartSeconds }];
17131    });
17132    const startSeconds = Number(source.startSeconds);
17133    const endSeconds = Number(source.endSeconds);
17134    return {
17135      fileName: String(source.fileName || '').trim().slice(0, 180),
17136      importedAt: String(source.importedAt || '').trim().slice(0, 50),
17137      startSeconds: Number.isInteger(startSeconds) && startSeconds >= 0 && startSeconds < 86400 ?startSeconds : null,
17138      endSeconds: Number.isInteger(endSeconds) && endSeconds >= 0 && endSeconds <= 604800 ?endSeconds : null,
17139      entries,
17140    };
17141  }
17142  defaultRosganOpsState(remate = this.state.currentRemate) {
17143    const rawContacts = remate?.telefonos || remate?.phones || remate?.contactPhones || remate?.contactos || remate?.contactoTelefono || remate?.contactPhone || remate?.contacto?.telefono || remate?.contact?.phone || '';
17144    const contacts = Array.isArray(rawContacts)
17145      ?rawContacts.map((item) => typeof item === 'string' ?item : (item?.telefono || item?.phone || '')).filter(
17145Boolean).join(' | ')
17146      : String(rawContacts || '').trim();
17147    return { commercial: {}, ratings: {}, salesOrder: this.normalizeRosganSalesOrderSchedule(null), tv: { selectedCert: '', visible: true, logosVisible: true, lowerThirdVisible: true, contactsVisible: true, contacts, lotVersions: {}, contactsByConsignataria: {}, vmixBrowserInputNumber: 0, vmixBrowserInputKey: '' } };
17148  }
17149  normalizeRosganOpsState(value, remate = this.state.currentRemate) {
17150    const base = this.defaultRosganOpsState(remate);
17151    const source = value && typeof value === 'object' ?value : {};
17152    return {
17153      commercial: source.commercial && typeof source.commercial === 'object' ?source.commercial : {},
17154      ratings: Object.fromEntries(Object.entries(source.ratings && typeof source.ratings === 'object' ?source.ratings : {}).flatMap(([cert, value]) => {
17155        const rating = Number(value);
17156        return cert && Number.isInteger(rating) && rating >= 1 && rating <= 6 ?[[String(cert), rating]] : [];
17157      })),
17158      salesOrder: this.normalizeRosganSalesOrderSchedule(source.salesOrder),
17159      tv: { ...base.tv, ...(source.tv && typeof source.tv === 'object' ?source.tv : {}) },
17160    };
17161  }
17162  loadRosganOpsState(remate = this.state.currentRemate) {
17163    try { return this.normalizeRosganOpsState(JSON.parse(localStorage.getItem(this.rosganOpsStorageKey(remate)) || '{}'), remate); }
17164    catch { return this.defaultRosganOpsState(remate); }
17165  }
17166  saveRosganOpsState(next, remate = this.state.currentRemate) {
17167    const normalized = this.normalizeRosganOpsState(next, remate);
17168    const patch = window.RosganOperationsPatch.diff(this.loadRosganOpsState(remate), normalized);
17169    if(this.remateRouteId(remate)===this.remateRouteId(this.state.currentRemate)){this._rosganOpsSignature=JSON.stringify(normalized);this._rosganOpsLocalUpdatedAt=Date.now();}
17170    try {
17171      localStorage.setItem(this.rosganOpsStorageKey(remate), JSON.stringify(normalized));
17172      window.dispatchEvent(new CustomEvent('rosgan:remate-operations', { detail: { remateId: this.remateRouteId(remate) } }));
17173    } catch {}
17174    if (this.remateRouteId(remate) === this.remateRouteId(this.state.currentRemate)) {
17175      this._rosganOpsLocalUpdatedAt = Date.now();
17176      this.setState((previous) => ({ rosganOpsDrafts: normalized.commercial, rosganOpsConfig: normalized.tv, rosganSalesOrderRatings: normalized.ratings, rosganSalesOrderSchedule: normalized.salesOrder, rosganOpsPulse: Number(previous.rosganOpsPulse || 0) + 1 }));
17177    }
17178    if(Object.keys(patch).length)this.queueRosganOpsRemoteSave(patch, remate);
17179    return normalized;
17180  }
17181  rosganOpsRemotePath(remate = this.state.currentRemate, tvOnly = false) {
17182    const id = this.remateRouteId(remate);
17183    return '/api/remate-operations/' + encodeURIComponent(id) + (tvOnly ?'?scope=tv' : '');
17184  }
17185  normalizeRosganOpsRemotePayload(data, remate = this.state.currentRemate, tvOnly = false) {
17186    const root = data?.data || data?.item || data || {};
17187    if (tvOnly) {
17188      const local = this.loadRosganOpsState(remate);
17189      return this.normalizeRosganOpsState({ commercial: local.commercial, ratings: local.ratings, salesOrder: local.salesOrder, tv: root.tv || root.state?.tv || {} }, remate);
17190    }
17191    return this.normalizeRosganOpsState(root.state || root, remate);
17192  }
17193  rosganOpsPendingWrites(remate, pending) {
17194    const key=this.rosganOpsStorageKey(remate)+':pending-writes';
17195    this._rosganOpsWriterId ||= crypto.randomUUID();
17196    try{
17197      const writers=JSON.parse(localStorage.getItem(key)||'{}'),now=Date.now();
17198      for(const id of Object.keys(writers))if(writers[id]<now)delete writers[id];
17199      if(pending===true)writers[this._rosganOpsWriterId]=now+60000;
17200      if(pending===false)delete writers[this._rosganOpsWriterId];
17201      if(pending!==undefined)localStorage.setItem(key,JSON.stringify(writers));
17202      return Object.keys(writers).length>0;
17203    }catch{return false;}
17204  }
17205  async fetchRosganOpsRemoteState(options = {}) {
17206    if (!this.canOpenRosganOperation(this.state.currentRemate, 'commercial')) return null;
17207    if (!['rosganSalesOrder', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig'].includes(this.state.route) || !this.state.currentRemate) return null;
17208    const now = Date.now();
17209    if (!options.force && this._lastRosganOpsRemoteFetchAt && now - this._lastRosganOpsRemoteFetchAt < 1800) return null;
17210    if (!options.force && this._rosganOpsLocalUpdatedAt && now - this._rosganOpsLocalUpdatedAt < 1400) return null;
17211    this._lastRosganOpsRemoteFetchAt = now;
17212    const remate = this.state.currentRemate;
17213    const tvOnly = this.state.route === 'rosganTvLower';
17214    if(this.rosganOpsPendingWrites(remate))return null;
17215    const localSignature=JSON.stringify(this.loadRosganOpsState(remate));
17216    const write = this._rosganOpsWrites?.[this.remateRouteId(remate)];
17217    if (write && (write.busy || write.revision !== write.acknowledged)) return null;
17218    const revision = write?.revision || 0;
17219    const request = this._rosganOpsReadSequence = (this._rosganOpsReadSequence || 0) + 1;
17220    try {
17221      const data = await this.apiFetch(this.rosganOpsRemotePath(remate, tvOnly), { cache: 'no-store' });
17222      if(this.rosganOpsPendingWrites(remate)||JSON.stringify(this.loadRosganOpsState(remate))!==localSignature)return null;
17223      const latestWrite = this._rosganOpsWrites?.[this.remateRouteId(remate)];
17224      if (request !== this._rosganOpsReadSequence || (latestWrite?.revision || 0) !== revision || latestWrite?.busy) return null;
17225      if (this._rosganOpsLocalUpdatedAt > now) return null;
17226      if (this.remateRouteId(remate) !== this.remateRouteId(this.state.currentRemate)) return null;
17227      const next = this.normalizeRosganOpsRemotePayload(data, remate, tvOnly);
17228      this.syncRosganTvLotVersions(next.tv, remate);
17229      const signature = JSON.stringify(next);
17230      if (signature && signature !== this._rosganOpsSignature) {
17231        this._rosganOpsSignature = signature;
17232        try { localStorage.setItem(this.rosganOpsStorageKey(remate), JSON.stringify(next)); } catch {}
17233        this.setState((previous) => ({ rosganOpsDrafts: next.commercial, rosganOpsConfig: next.tv, rosganSalesOrderRatings: next.ratings, rosganSalesOrderSchedule: next.salesOrder, rosganOpsPulse: Number(previous.rosganOpsPulse || 0) + 1 }), () => {
17234          if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo();
17235        });
17236      }
17237      return next;
17238    } catch (error) {
17239      if (!options.silent && error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo sincronizar la operación del remate.');
17240      return null;
17241    }
17242  }
17243  queueRosganOpsRemoteSave(state, remate = this.state.currentRemate) {
17244    const id = this.remateRouteId(remate);
17245    this._rosganOpsWrites ||= {};
17246    const write = this._rosganOpsWrites[id] ||= { revision: 0, acknowledged: 0 };
17247    write.revision += 1;
17248    this.rosganOpsPendingWrites(remate,true);
17249    // Keep the most recent selection even while a previous PUT is still in flight.
17250    write.pending = window.RosganOperationsPatch.merge(write.pending, state);
17251    if (write.busy) return;
17252    clearTimeout(write.timer);
17253    write.timer = setTimeout(() => { const pending=write.pending;write.pending=null;this.saveRosganOpsRemoteState(pending, remate); }, 350);
17254  }
17255  async saveRosganOpsRemoteState(state, remate = this.state.currentRemate) {
17256    if (!remate || !this.canManageCabanaPrivatePages()) return;
17257    const id = this.remateRouteId(remate);
17258    this._rosganOpsWrites ||= {};
17259    const write = this._rosganOpsWrites[id] ||= { revision: 0, acknowledged: 0 };
17260    if (write.busy) { write.pending = window.RosganOperationsPatch.merge(write.pending, state); return; }
17261    write.busy = true;
17262    this.rosganOpsPendingWrites(remate,true);
17263    const revision = write.revision;
17264    try {
17265      await this.apiFetch(this.rosganOpsRemotePath(remate), { method: 'PATCH', body: JSON.stringify({ protocol:2, patch:state }) });
17266      write.acknowledged = revision;
17267      // Polling compares against the actual local state, never a response snapshot.
17268    } catch (error) {
17269      write.pending = window.RosganOperationsPatch.merge(state,write.pending);
17270      if (error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo guardar la operación del remate.');
17271      write.retry = true;
17272    } finally {
17273      write.busy = false;
17274      if(!write.pending)this.rosganOpsPendingWrites(remate,false);
17275      if (write.pending) {
17276        const retry=write.retry;write.retry=false;clearTimeout(write.timer);
17277        write.timer=setTimeout(()=>{const pending=write.pending;write.pending=null;this.saveRosganOpsRemoteState(pending,remate);},retry?3000:0);
17278      }
17279    }
17280  }
17281  syncRosganOpsExternalState() {
17282    if (!['rosganSalesOrder', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig'].includes(this.state.route) || !this.state.currentRemate) return;
17283    const next = this.loadRosganOpsState(this.state.currentRemate);
17284    this.syncRosganTvLotVersions(next.tv);
17285    const signature = JSON.stringify(next);
17286    if (signature === this._rosganOpsSignature) return;
17287    this._rosganOpsSignature = signature;
17288    this.setState((previous) => ({ rosganOpsDrafts: next.commercial, rosganOpsConfig: next.tv, rosganSalesOrderRatings: next.ratings, rosganSalesOrderSchedule: next.salesOrder, rosganOpsPulse: Number(previous.rosganOpsPulse || 0) + 1 }), () => {
17289      if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo();
17290    });
17291  }
17292  rosganOpsRoute(action) {
17293    return ({ stats:'rosganStats', commercial: 'rosganCommercial', prices: 'rosganPriceBoard', tv: 'rosganTvLower', tvConfig: 'rosganTvConfig' })[action] || 'remateDetail';
17294  }
17295  async updateRosganAirtime(body){
17296    if(this._rosganAirtimeSaving)return;
17297    const id=this.remateRouteId(this.state.currentRemate);
17298    this._rosganAirtimeSaving=true;
17299    try{
17300      const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/airtime',{method:'PATCH',body:JSON.stringify(body)});
17301      if(this.remateRouteId(this.state.currentRemate)===id)this.setState({rosganAirtime:{...result.data,receivedAt:Date.now()},rosganTimeEdit:null});
17302    }catch(error){this.showToast(error.message||'No se pudo guardar el tiempo.');}
17303    finally{this._rosganAirtimeSaving=false;}
17304  }
17305  saveRosganManualTime(cert){
17306    const value=String(this.state.rosganTimeEdit?.value||'').trim();
17307    if(!/^\d{1,2}:[0-5]\d:[0-5]\d$/.test(value)){this.showToast('Ingresá el tiempo como HH:MM:SS, por ejemplo 00:02:30.');return;}
17308    const [h,m,s]=value.split(':').map(Number);
17309    this.updateRosganAirtime({action:'set',cert,milliseconds:(h*3600+m*60+s)*1000});
17310  }
17311  async refreshRosganLotInterest(){
17312    if(this.state.route!=='rosganCommercial'||this._rosganInterestBusy)return;
17313    const id=this.remateRouteId(this.state.currentRemate);
17314    if(!id||(this._rosganInterestId===id&&Date.now()-(this._rosganInterestAt||0)<10000))return;
17315    this._rosganInterestId=id;this._rosganInterestAt=Date.now();this._rosganInterestBusy=true;
17316    try{
17317      const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/interest');
17318      if(this.remateRouteId(this.state.currentRemate)===id&&JSON.stringify(result.data)!==JSON.stringify(this.state.rosganLotInterest))this.setState({rosganLotInterest:result.data});
17319    }catch(error){if(this.remateRouteId(this.state.currentRemate)===id)this.setState({rosganLotInterest:null});}
17320    finally{this._rosganInterestBusy=false;}
17321  }
17322  async refreshRosganAirtime() {
17323    const route=this.state.route,id=this.remateRouteId(this.state.currentRemate);
17324    if(!id||this._rosganAirtimeSaving||this._rosganAirtimeBusy||!['rosganCommercial','rosganTvLower'].includes(route))return;
17325    const path='/api/remate-operations/'+encodeURIComponent(id)+'/airtime';
17326    this._rosganAirtimeBusy=true;
17327    try {
17328      if(route==='rosganTvLower'){
17329        const outputId=new URLSearchParams(location.search).get('airtime');
17330        if(!outputId)return;
17331        this._rosganAirtimeSession ||= (crypto.randomUUID?crypto.randomUUID():Date.now()+'-'+Math.random().toString(36).slice(2));
17332        const plateTv=this.loadRosganOpsState().tv;
17333        const certs=plateTv.videoVisible===false||(plateTv.visible!==false&&['waiting','end'].includes(plateTv.fullScreenPlate))?[]:this._rosganTvAirSlots?.length?this._rosganTvAirSlots.map(slot=>String(slot.cert)):this._rosganTvDecks?.onAirCert?[String(this._rosganTvDecks.onAirCert)]:[];
17334        await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({outputId,sessionId:this._rosganAirtimeSession,certs}),signal:AbortSignal.timeout(2500)});
17335      } else {
17336        const result=await this.apiFetch(path,{signal:AbortSignal.timeout(3000)});
17337        if(!this._rosganAirtimeSaving&&this.remateRouteId(this.state.currentRemate)===id){
17338          const previous=this.state.rosganAirtime,next={...result.data,receivedAt:Date.now()};
17339          const signature=value=>JSON.stringify([value?.remateId,value?.totals,value?.running,value?.reason,value?.outputConnected]);
17340          if(Date.now()-(this._rosganPriceTypingAt||0)>400&&(next.running?.length||!previous?.receivedAt||signature(next)!==signature(previous)))this.setState({rosganAirtime:next});
17341          else if(previous&&signature(next)===signature(previous))Object.assign(previous,next);
17342        }
17343      }
17344    }catch(error){if(route==='rosganCommercial')this.setState({rosganAirtime:this.state.rosganAirtime?{...this.state.rosganAirtime,receivedAt:0,running:[]}:null});}
17345    finally {this._rosganAirtimeBusy=false;}
17346  }
17347  rosganOpsSuffix(action) {
17348    return ({ stats:'estadisticas', commercial: 'comercial', prices: 'precios', tv: 'tv', tvConfig: 'tv/config' })[action] || '';
17349  }
17350  async fetchRosganOfficialPrices(remate,options={}) {
17351    const key=this.remateRouteId(remate),current=this.state.rosganOfficialPrices?.[key];
17352    if(!key||current?.loading||(!options.force&&current?.groups?.length))return;
17353    this.setState({rosganOfficialPrices:{...(this.state.rosganOfficialPrices||{}),[key]:{...current,loading:true,error:''}}});
17354    try{
17355      const candidates=await this.unwrap(window.RosganNetPriceResults.rematesUrl(remate));
17356      const match=window.RosganNetPriceResults.findRemate(candidates,remate);
17357      if(!match?.id_remate)throw new Error('RosganNet no encontró un remate equivalente por fecha y nombre.');
17358      const payload=await this.unwrap(window.RosganNetPriceResults.resultsUrl(match.id_remate));
17359      const groups=window.RosganNetPriceResults.groups(payload);
17360      if(!groups.length)throw new Error('RosganNet todavía no publicó resultados para este remate.');
17361      this.setState({rosganOfficialPrices:{...(this.state.rosganOfficialPrices||{}),[key]:{loading:false,error:'',groups,rosganNetRemateId:String(match.id_remate),loadedAt:Date.now()}}});
17362    }catch(error){
17363      this.setState({rosganOfficialPrices:{...(this.state.rosganOfficialPrices||{}),[key]:{...current,loading:false,error:error?.message||'No se pudieron cargar los resultados oficiales.'}}});
17364    }
17365  }
17366  showRematePanelPicker() {
17367    if (!this.canManageRosganLots() || this._rematePanelDialog?.isConnected) return;
17368    const previousFocus = document.activeElement;
17369    const dialog = document.createElement('dialog');
17370    dialog.id = 'remate-panel-picker';
17371    dialog.setAttribute('aria-labelledby', 'remate-panel-title');
17372    dialog.setAttribute('aria-describedby', 'remate-panel-description');
17373    dialog.style.cssText = 'width:min(600px,calc(100vw - 28px));max-height:calc(100dvh - 32px);padding:0;box-sizing:border-box;border:1px solid #dce7f2;border-radius:22px;background:#fff;color:#112033;box-shadow:0 28px 90px #001d3b45;font-family:inherit;overflow:auto;';
17374    dialog.innerHTML = `<style>
17375      #remate-panel-picker::backdrop{background:rgba(0,25,52,.52);backdrop-filter:blur(4px)}
17376      #remate-panel-picker .rp-heading{padding:24px 24px 18px;border-bottom:1px solid #e5edf5;position:sticky;top:0;background:#fff;z-index:1}
17377      #remate-panel-picker .rp-list{padding:16px;display:grid;gap:10px}
17378      #remate-panel-picker .rp-choice{width:100%;display:flex;align-items:center;gap:14px;padding:17px;text-align:left;border:1px solid #dce7f2;border-radius:14px;background:#f8fafd;color:#112033;font:inherit;cursor:pointer;transition:background .15s,border-color .15s}
17379      #remate-panel-picker .rp-choice:hover{background:#edf5ff;border-color:#0a5ca8}
17380      #remate-panel-picker button:focus-visible{outline:3px solid #0a5ca8;outline-offset:2px}
17381      #remate-panel-picker .rp-icon{width:42px;height:42px;border-radius:12px;background:#e7f0fa;display:grid;place-items:center;flex:none}
17382      #remate-panel-picker strong{display:block;font-size:15px;line-height:1.4;overflow-wrap:anywhere}
17383      #remate-panel-picker small{display:block;margin-top:5px;color:#65758a;font-size:12px;line-height:1.5}
17384      #remate-panel-picker .rp-message{margin:0;padding:22px 8px;color:#65758a;font-size:14px;line-height:1.5}
17385      @media(max-width:480px){#remate-panel-picker .rp-heading{padding:20px 18px 16px}#remate-panel-picker .rp-list{padding:12px}#remate-panel-picker .rp-choice{padding:14px;gap:10px}}
17386    </style><header class="rp-heading"><div style="display:flex;align-items:center;justify-content:space-between;gap:14px"><h2 id="remate-panel-title" style="margin:0;font-size:23px;letter-spacing:-.02em"></h2><button type="button" data-close aria-label="Cerrar selector de remates" style="width:36px;height:36px;border:1px solid #dce7f2;border-radius:50%;background:#fff;color:#12304d;font-size:24px;cursor:pointer;flex:none">×</button></div><p id="remate-panel-description" style="margin:9px 0 0;color:#65758a;font-size:14px">Elegí el remate para abrir su panel.</p></header><div class="rp-list" data-remates></div>`;
17387    dialog.querySelector('h2').textContent = this.isRosganProfile() ? 'Panel Consignatario' : 'Panel Auditor';
17388    dialog.querySelector('[data-close]').onclick = () => dialog.close();
17389    dialog.addEventListener('click', event => {
17390      if (event.target !== dialog) return;
17391      const r = dialog.getBoundingClientRect();
17392      if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) dialog.close();
17393    });
17394    dialog.addEventListener('close', () => {
17395      this._rematePanelRequest = (this._rematePanelRequest || 0) + 1;
17396      this._rematePanelDialog = null;
17397      this.setState({ rematePanelPickerOpen: false, rematePanelPickerLoading: false });
17398      dialog.remove();
17399      if (previousFocus?.isConnected) previousFocus.focus();
17400    }, { once: true });
17401    this._rematePanelDialog = dialog;
17402    document.body.appendChild(dialog);
17403    dialog.showModal();
17404    this.loadRematePanelPicker();
17405  }
17406  renderRematePanelDialog() {
17407    const dialog = this._rematePanelDialog;
17408    if (!dialog?.isConnected) return;
17409    if (this.state.rematePanelScopeUid !== this.state.user?.uid || !this.canManageRosganLots()) { dialog.close(); return; }
17410    const list = dialog.querySelector('[data-remates]');
17411    list.replaceChildren();
17412    const message = text => { const p = document.createElement('p'); p.className = 'rp-message'; p.setAttribute('role', 'status'); p.textContent = text; list.appendChild(p); };
17413    if (this.state.rematePanelPickerLoading) { message('Cargando tus remates…'); return; }
17414    if (this.state.rematePanelPickerError) {
17415      message(this.state.rematePanelPickerError);
17416      const retry = document.createElement('button'); retry.className = 'rp-choice'; retry.textContent = 'Reintentar'; retry.onclick = () => this.loadRematePanelPicker(); list.appendChild(retry); return;
17417    }
17418    const items = [...(this.state.rematePanelItems || [])].sort((a, b) => (Date.parse(a.fechaHoraRemate) || Infinity) - (Date.parse(b.fechaHoraRemate) || Infinity));
17419    if (!items.length) { message('No tenés remates disponibles para administrar.'); return; }
17420    for (const item of items) {
17421      const button = document.createElement('button'); button.type = 'button'; button.className = 'rp-choice';
17422      button.innerHTML = '<span class="rp-icon"><img src="/assets/icons/lucide/clipboard-list.svg" width="21" height="21" alt=""></span><span style="flex:1;min-width:0"><strong></strong><small></small></span><span aria-hidden="true" style="color:#0a5ca8;font-size:21px">→</span>';
17423      button.querySelector('strong').textContent = item.titulo || item.nombre_remate || item.nombre || this.remateRouteId(item);
17424      const date = new Date(item.fechaHoraRemate);
17425      button.querySelector('small').textContent = [Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString('es-AR', { day:'numeric', month:'long', year:'numeric' }), item.localidad].filter(Boolean).join(' · ');
17426      button.onclick = () => this.openSelectedRematePanel(this.remateRouteId(item));
17427      list.appendChild(button);
17428    }
17429  }
17430  rosganPanelRemates(remates, relations = []) {
17431    if (!this.canManageRosganLots()) return [];
17432    const admin = this.canManageUsers();
17433    const keys = new Set(relations.filter(r => r && r.isActive !== false).flatMap(r => [r.id, r.consignatariaId, r.slug, r.ref, r.name]).filter(Boolean).map(v => this.consKey(v)));
17434    const seen = new Set();
17435    return this.rosganUpcomingRemates(remates).filter(item => {
17436      const id = this.remateRouteId(item);
17437      if (!id || seen.has(id)) return false;
17438      const participants = Array.isArray(item.consignatariasParticipantes) ? item.consignatariasParticipantes : [];
17439      const refs = [item.consignatariaId, item.consignatariaNombre, ...participants.flatMap(p => [p.consignatariaId, p.id, p.consignatariaNombre, p.name])];
17440      const allowed = admin || (keys.size > 0 && (this.consKey(item.modoConsignataria) === 'todos' || refs.some(ref => ref && keys.has(this.consKey(ref)))));
17441      if (allowed) seen.add(id);
17442      return allowed;
17443    });
17444  }
17445  async loadRematePanelPicker() {
17446    if (!this.canManageRosganLots()) return;
17447    const uid = this.state.user.uid, epoch = this._sessionEpoch || 0;
17448    const request = this._rematePanelRequest = (this._rematePanelRequest || 0) + 1;
17449    const current = () => request === this._rematePanelRequest && uid === this.state.user?.uid && epoch === (this._sessionEpoch || 0) && this.canManageRosganLots();
17450    this.setState({ rematePanelPickerOpen: true, rematePanelPickerLoading: true, rematePanelPickerError: '', rematePanelScopeUid: uid, rematePanelItems: [], rematePanelSelectedId: '' });
17451    this.renderRematePanelDialog();
17452    try {
17453      const [response, relations] = await Promise.all([
17454        this.apiFetch('/api/Remates', { cache: 'no-store' }),
17455        this.canManageUsers() ? Promise.resolve([]) : this.fetchUserConsignataria(uid),
17456      ]);
17457      if (!current()) return;
17458      if (!Array.isArray(relations)) throw new Error('No se pudieron verificar tus consignatarias. Reintentá.');
17459      const items = this.rosganPanelRemates(this.rosganArray(response), relations);
17460      this.setState({ rematePanelItems: items, rematePanelPickerLoading: false, rematePanelSelectedId: items.length === 1 ? this.remateRouteId(items[0]) : '' });
17461      this.renderRematePanelDialog();
17462    } catch (error) {
17463      if (current()) this.setState({ rematePanelPickerLoading: false, rematePanelPickerError: error?.message || 'No se pudieron cargar tus remates.' });
17464      if (current()) this.renderRematePanelDialog();
17465    }
17466  }
17467  openSelectedRematePanel(selectedId = this.state.rematePanelSelectedId) {
17468    if (!this.canManageRosganLots() || this.state.rematePanelPickerLoading || this.state.rematePanelScopeUid !== this.state.user?.uid) return;
17469    const relations = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(
17469Boolean);
17470    const item = this.rosganPanelRemates(this.state.rematePanelItems, relations).find(r => this.remateRouteId(r) === selectedId);
17471    if (!item) { this.showToast('Seleccioná un remate disponible para tu cuenta.'); return; }
17472    this.setState({ rematePanelPickerOpen: false });
17473    this._rematePanelDialog?.close();
17474    this.openRosganAuditor(item);
17475  }
17476  async openRosganAuditorById(remateId) {
17477    if (!this.state.apiUser) { this.setState({ rosganAuditorRouteError: 'No se pudo validar la sesión. Reintentá para abrir el panel.' }); return; }
17478    if (this._auditorLoadingId === String(remateId)) return;
17479    this._auditorLoadingId = String(remateId);
17480    const request = this._auditorOpenRequest = (this._auditorOpenRequest || 0) + 1;
17481    this.setState({ route: 'rosganAuditor', rosganAuditorRouteError: '' });
17482    const controller = new AbortController();
17483    const timeout = setTimeout(() => controller.abort(), 20000);
17484    try {
17485      const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId), { signal: controller.signal });
17486      if (request !== this._auditorOpenRequest || this.rematePathTarget()?.action !== 'auditor' || this.rematePathTarget()?.remateId !== String(remateId)) return;
17487      const item = response?.data ?? response;
17488      if (!item || item.cabanasConfig) throw new Error('Este panel corresponde a remates Rosgan.');
17489      this.openRosganAuditor(item, { syncPath: false });
17490    } catch (error) { if (request === this._auditorOpenRequest && this.rematePathTarget()?.action === 'auditor') this.setState({ rosganAuditorRouteError: error.message || 'No se pudo abrir el panel.' }); }
17491    finally { clearTimeout(timeout); if (request === this._auditorOpenRequest) this._auditorLoadingId = ''; }
17492  }
17493  openRosganAuditor(item, options = {}) {
17494    if (!this.state.user || !this.state.apiUser) { this.showToast('Iniciá sesión para consultar tus lotes.'); return; }
17495    const remate = this.normalizeRosganActiveRemate(item);
17496    this.setState({ route: 'rosganAuditor', rosganAuditorRouteError: '' });
17497    this._auditorPanel?.close();
17498    if (options.syncPath !== false) this.syncRemateSectionLocation(remate, 'auditor');
17499    this._auditorPanel = window.openRosganAuditor({
17500      remateId: this.remateRouteId(remate), title: remate.nombre_remate, remateDate: remate.fechaHoraRemate,
17501      lotLoadingEnabled: remate.cargaDeLotes !== false,
17502      onLotLoadingChange: enabled => this.setRemateLotLoading(remate, enabled),
17503      canChangeStatus: this.canManageUsers(this.state.apiUser),
17504      canBulkControl: this.apiRoleIds(this.state.apiUser).includes('consignatario'),
17505      onSalesOrder: this.canManageUsers(this.state.apiUser) ? () => {
17506        if (!this.canManageUsers(this.state.apiUser)) { this.showToast('No tenés permisos para administrar el orden de venta.'); return; }
17507        this._auditorPanel?.close(); this._auditorPanel = null;
17508        this.openRosganSalesOrderPage(remate, null, true, true);
17509      } : null,
17510      consignatariaBrand: (lot) => {
17511        const record = (this.state.rosganLotConsignatarias || []).find(item => String(item.id) === String(lot.consignatariaId));
17512        const name = lot.consignatariaName || record?.name || record?.nombre || this.consRecord(lot.consignatariaId)?.name || '';
17513        return { name: name || 'Consignataria sin informar', logo: this.consLogo(name || lot.consignatariaId), whiteLogo: this.consWhiteLogo(name || lot.consignatariaId), color: this.consBrandColor(name || lot.consignatariaId) };
17514      },
17515      apiFetch: (path, opts) => this.apiFetch(path, opts),
17516      onDuplicate: (lot) => this.openRosganLotEditor(lot, remate, { duplicate: true }),
17517      onClose: () => { this._auditorPanel = null; this.openRosganActiveRemate(remate); },
17518    });
17519  }
17520  canOpenRosganOperation(item, action) {
17521    if (action === 'prices') return true;
17522    if (!this.state.user || !this.state.apiUser) return false;
17523    if (this.canManageUsers()) return true;
17524    if (action !== 'stats' || !this.apiRoleIds(this.state.apiUser).includes('consignatario')) return false;
17525    const memberships = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(
17525Boolean);
17526    if (item?.modoConsignataria === 'Todos') return memberships.some(c => c.id || c.consignatariaId || c.ref);
17527    return this.canExportCabanaCatalog(item, this.state.apiUser);
17528  }
17529  openRosganOperations(item, action = 'commercial', options = {}) {
17530    const remate = this.normalizeRosganActiveRemate(item || {});
17531    const route = this.rosganOpsRoute(action);
17532    if (!this.canOpenRosganOperation(remate, action)) {
17533      this.showToast('Esta herramienta está disponible para perfiles autorizados.');
17534      if (options.syncPath !== false) this.syncRemateLocation(remate);
17535      this.openRosganActiveRemate(remate, { syncPath: false });
17536      return;
17537    }
17538    if (options.syncPath !== false) this.syncRemateSectionLocation(remate, this.rosganOpsSuffix(action));
17539    const stored = this.loadRosganOpsState(remate);
17540    if (action === 'tvConfig') {
17541      Promise.all([this.apiFetch('/api/rosgan-categories'), this.apiFetch('/api/rosganBreeds')]).then(([categories,breeds]) => {
17542        this.setState({ rosganLotCategories: this.rosganArray(categories), rosganLotBreeds: this.rosganArray(breeds) });
17543      }).catch(() => this.showToast('No se pudieron cargar las categorías y razas.'));
17544    }
17545    const sameRemate = this.remateRouteId(this.state.currentRemate) === this.remateRouteId(remate);
17546    this._rosganOpsSignature = JSON.stringify(stored);
17547    this.setState({ route, currentRemate: remate, rdLightbox: null, rosganOpsDrafts: stored.commercial, rosganOpsConfig: stored.tv, rosganOpsLoading: !sameRemate || !Array.isArray(this.state.remateLots) }, () => {
17548      window.scrollTo(0, 0);
17549      if(action!=='stats')this.fetchRosganOpsRemoteState({ force: true, silent: true });
17550      if(action==='prices')this.fetchRosganOfficialPrices(remate,{force:true});
17551      if(action==='tv'||action==='tvConfig')this.fetchCatalogPublication(remate);
17552      if (!sameRemate || !Array.isArray(this.state.remateLots)) this.fetchRosganActiveLots(remate).finally(() => this.setState({ rosganOpsLoading: false }));
17553      else if (route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo();
17554    });
17555  }
17556  async openRosganOperationsById(remateId, action = 'commercial') {
17557    const requestedPath = window.location.pathname;
17558    try {
17559      const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId));
17560      if (window.location.pathname !== requestedPath) return;
17561      const item = response?.data ?? response;
17562      if (!item || item?.cabanasConfig) throw new Error('No es un remate Rosgan.');
17563      return this.openRosganOperations(item, action, { syncPath: false });
17564    } catch (error) {
17565      if (window.location.pathname !== requestedPath) return;
17566      this.showToast(error?.message || 'No se pudo abrir la herramienta del remate.');
17567    }
17568  }
17569  backToRosganCatalog() {
17570    const remate = this.state.currentRemate;
17571    if (!remate) return this.go('remates');
17572    this.syncRemateLocation(remate);
17573    this.setState({ route: 'remateDetail', rdLightbox: null });
17574    window.scrollTo(0, 0);
17575  }
17576  openCurrentRosganOps(action) {
17577    if (this.state.currentRemate) this.openRosganOperations(this.state.currentRemate, action);
17578  }
17579  editRosganCommercialPrice(lot,priceMode,event){
17580    const value=window.RosganPriceInput.input(event.target),remate=this.state.currentRemate;
17581    const cert=this.remateLotRouteId(lot),key=this.remateRouteId(remate)+'/'+cert;
17582    this._rosganPriceEdits ||= new Map();
17583    const previous=this._rosganPriceEdits.get(key);clearTimeout(previous?.timer);
17584    const edit={remate,cert,patch:{...previous?.patch,priceMode,[priceMode==='kilo'?'precioKilo':'precioCabeza']:value}};
17585    this._rosganPriceEdits.set(key,edit);
17586    this._rosganPriceTypingAt=Date.now();
17587    edit.timer=setTimeout(()=>this.flushRosganCommercialPrice(key),350);
17588  }
17589  openRosganLotDivisions(lot){
17590    const remate=this.state.currentRemate,key=this.remateRouteId(remate)+'/'+this.remateLotRouteId(lot);
17591    this.flushRosganCommercialPrice(key);
17592    const sale=this.loadRosganOpsState(remate).commercial[this.remateLotRouteId(lot)]||{};
17593    window.openRosganSaleDivisions({lot,sale,mode:window.RosganPriceInput.mode(lot,sale),onSave:(divisions,priceMode)=>{
17594      if(this.remateRouteId(this.state.currentRemate)!==this.remateRouteId(remate))throw new Error('Cambió el remate abierto. Volvé a abrir las divisiones del lote.');
17595      const quantity=Number(lot.cantidad),priced=divisions.filter(p=>p.price>0),soldQuantity=priced.reduce((s,p)=>s+p.quantity,0);
17596      const average=soldQuantity?priced.reduce((s,p)=>s+p.quantity*p.price,0)/soldQuantity:0;
17597      this.updateRosganCommercial(lot,{divisions,divisionsQuantity:quantity,priceMode,precioKilo:priceMode==='kilo'?String(average):'',precioCabeza:priceMode==='cabeza'?String(average):'',comprador:'',status:soldQuantity===quantity?'vendido':'pendiente'});
17598    }});
17599  }
17600  editRosganCommercialBuyer(lot,event){
17601    const remate=this.state.currentRemate,cert=this.remateLotRouteId(lot),key=this.remateRouteId(remate)+'/'+cert;
17602    this._rosganPriceEdits ||= new Map();
17603    const previous=this._rosganPriceEdits.get(key);clearTimeout(previous?.timer);
17604    const edit={remate,cert,patch:{...previous?.patch,comprador:String(event.target.value||'').slice(0,200)}};
17605    this._rosganPriceEdits.set(key,edit);this._rosganPriceTypingAt=Date.now();
17606    edit.timer=setTimeout(()=>this.flushRosganCommercialPrice(key),600);
17607  }
17608  async downloadRosganBilling(format){
17609    if(this._rosganBillingBusy)return;
17610    this._rosganBillingBusy=true;
17611    try{
17612      const remate=this.state.currentRemate,commercial={...this.loadRosganOpsState(remate).commercial};
17613      for(const edit of this._rosganPriceEdits?.values()||[])if(this.remateRouteId(edit.remate)===this.remateRouteId(remate))commercial[edit.cert]={...commercial[edit.cert],...edit.patch};
17614      const report=window.RosganBilling.build(this.state.remateLots||[],commercial,{mode:(lot,sale)=>window.RosganPriceInput.mode(lot,sale),consignee:name=>this.consDisplayName(name)});
17615      if(!report.rows.length){this.showToast('Todavía no hay lotes vendidos para facturar.');return;}
17616      const meta={title:remate.nombre_remate||remate.title||'Remate',date:this.remateDate(remate)?.toLocaleDateString('es-AR')||'Fecha a confirmar'};
17617      const logoData=async(src,wide=false,background='')=>{try{const image=await this.loadCabanaCanvasImage(src);if(!image)return null;const canvas=document.createElement('canvas');canvas.width=wide?360:128;canvas.height=128;const ctx=canvas.getContext('2d');if(background){ctx.fillStyle=background;ctx.fillRect(0,0,canvas.width,canvas.height);}this.drawCabanaImageContain(ctx,image,0,0,canvas.width,canvas.height);return canvas.toDataURL('image/png');}catch{return null;}};
17618      meta.logo=await logoData('/assets/rosgan-logo-blue.png',true);
17619      meta.logos=Object.fromEntries(await Promise.all(report.groups.map(async group=>[group.consignee,await logoData(this.consLogo(group.consignee))||await logoData(this.consWhiteLogo(group.consignee),false,this.consColor(group.consignee))])));
17620      let buffer;
17621      if(format==='xlsx')buffer=await window.RosganBilling.excel(report,meta,await this.ensureCabanaExcelJs());
17622      else{
17623        for(const src of ['/assets/vendor/jspdf.umd.min.js','/assets/vendor/jspdf.plugin.autotable.min.js']){
17624          this._rosganBillingScripts ||= {};
17625          if(!this._rosganBillingScripts[src])this._rosganBillingScripts[src]=new Promise((resolve,reject)=>{const tag=document.createElement('script');tag.src=src;tag.onload=resolve;tag.onerror=()=>{delete this._rosganBillingScripts[src];reject(new Error('No se pudo cargar el generador PDF.'));};document.head.appendChild(tag);});
17626          await this._rosganBillingScripts[src];
17627        }
17628        buffer=window.RosganBilling.pdf(report,meta,window.jspdf.jsPDF,(doc,options)=>doc.autoTable(options));
17629      }
17630      this.downloadCabanaBlob(new Blob([buffer],{type:format==='xlsx'?'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'application/pdf'}),this.cabanaExportFileBase(remate)+'-facturacion.'+format);
17631      this.showToast('Informe de facturación descargado.');
17632    }catch(error){this.showToast(error.message||'No se pudo generar el informe.');}
17633    finally{this._rosganBillingBusy=false;}
17634  }
17635  commitRosganCommercialPrice(lot,priceMode,event){
17636    if(event.key!=='Enter'||event.isComposing||event.repeat)return;
17637    event.preventDefault();
17638    const value=String(event.target.value||'').replace(/\D/g,'');
17639    const inputs=Array.from(document.querySelectorAll('.rg-commercial-row .commercial-price input'));
17640    const nextLabel=inputs[inputs.indexOf(event.target)+1]?.getAttribute('aria-label');
17641    this.updateRosganCommercial(lot,{priceMode,[priceMode==='kilo'?'precioKilo':'precioCabeza']:value,status:Number(value)>0?'vendido':'no-vendido'});
17642    setTimeout(()=>{
17643      const next=Array.from(document.querySelectorAll('.rg-commercial-row .commercial-price input')).find(input=>nextLabel&&input.getAttribute('aria-label')===nextLabel);
17644      if(next){next.focus();next.select();}else event.target.blur();
17645    },40);
17646  }
17647  flushRosganCommercialPrice(key){
17648    const edit=this._rosganPriceEdits?.get(key);if(!edit)return;
17649    clearTimeout(edit.timer);this._rosganPriceEdits.delete(key);
17650    const current=this.loadRosganOpsState(edit.remate);
17651    current.commercial={...current.commercial,[edit.cert]:{...current.commercial[edit.cert],...edit.patch,updatedAt:new Date().toISOString()}};
17652    this.saveRosganOpsState(current,edit.remate);
17653  }
17654  updateRosganCommercial(lot, patch = {}) {
17655    const cert = this.remateLotRouteId(lot);
17656    if (!cert) return;
17657    const current = this.loadRosganOpsState();
17658    const key=this.remateRouteId(this.state.currentRemate)+'/'+cert,edit=this._rosganPriceEdits?.get(key);
17659    if(edit){clearTimeout(edit.timer);this._rosganPriceEdits.delete(key);}
17660    const previous = current.commercial[cert] || {};
17661    current.commercial = { ...current.commercial, [cert]: { ...previous, priceMode:window.RosganPriceInput.mode(lot,previous), ...edit?.patch, ...patch, updatedAt: new Date().toISOString() } };
17662    this.saveRosganOpsState(current);
17663  }
17664  rosganTvCatalogRows(rows,tv,selectedCert,query){
17665    const remateId=this.remateRouteId(this.state.currentRemate);
17666    if(this._rosganTvCatalogCache?.remateId!==remateId||this._rosganTvCatalogCache?.consignatarias!==this.state.consignatarias)this._rosganTvCatalogCache={remateId,consignatarias:this.state.consignatarias,brands:new Map(),rows:new Map()};
17667    const cache=this._rosganTvCatalogCache,needle=this.consKey(query),live=new Set(rows.map(row=>row.cert));
17668    for(const key of cache.rows.keys())if(!live.has(key))cache.rows.delete(key);
17669    return [...rows].sort((a,b)=>Number(a.saleOrder ?? a.lotNumber ?? a.order)-Number(b.saleOrder ?? b.lotNumber ?? b.order)||Number(a.lotNumber ?? a.order)-Number(b.lotNumber ?? b.order)).flatMap(row=>{
17670      const catalogConsignataria=row.tvConsignataria||row.consignataria;
17671      const signature=JSON.stringify([row.saleOrder,row.lotNumber,row.order,row.category,catalogConsignataria,row.tvConsignatariaLogo,row.tvConsignatariaColor,row.status,row.cert===selectedCert,[0,1,2].map(i=>tv.extraSlots?.[i]===row.cert)]);
17672      let cached=cache.rows.get(row.cert);
17673      if(!cached||cached.signature!==signature){
17674        let brand=row.tvConsignatariaLogo?{logo:row.tvConsignatariaLogo,color:row.tvConsignatariaColor}:cache.brands.get(catalogConsignataria);
17675        if(!brand){brand={logo:this.consWhiteLogo(catalogConsignataria)||this.consLogo(catalogConsignataria),color:this.consBrandColor(catalogConsignataria)};cache.brands.set(catalogConsignataria,brand);}
17676        cached={signature,search:this.consKey([row.order,row.cert,row.category,catalogConsignataria].join(' ')),value:{...row,...brand,consignataria:catalogConsignataria,
17677          disabled:row.status==='levantado',sold:row.status==='vendido',
17678          slots:[2,3,4].map(number=>({number,active:tv.extraSlots?.[number-2]===row.cert,disabled:row.cert===selectedCert||row.status==='levantado',className:tv.extraSlots?.[number-2]===row.cert?'is-active':'',label:(tv.extraSlots?.[number-2]===row.cert?'Quitar lote ':'Mostrar lote ')+row.order+' en pantalla '+number,select:()=>this.toggleRosganTvSlot(row.cert,number)})),
17679          selectedClass:(row.cert===selectedCert?'is-selected ':'')+(row.status==='levantado'?'is-lifted':''),select:()=>this.setRosganTvConfig({selectedCert:row.cert})}};
17680        cache.rows.set(row.cert,cached);
17681      }
17682      return cached.search.includes(needle)?[cached.value]:[];
17683    });
17684  }
17685  setRosganNameDraft(field,value) {
17686    if(!['rosNameDraft','rosRoleDraft'].includes(field))return;
17687    // Keep native typing immediate; only explicit Save/Edit needs a render.
17688    this.state={...this.state,[field]:String(value||'').slice(0,field==='rosNameDraft'?100:120)};
17689  }
17690  rosganNameStrapPresentation(tv) {
17691    const remate=this.remateRouteId(this.state.currentRemate);
17692    const person=tv.visible!==false?(tv.nameStraps||[]).find(p=>p.id===tv.activeNameStrap):null;
17693    if(person)this._rosganNameStrapLast={remate,name:person.name,role:person.role||''};
17694    const copy=this._rosganNameStrapLast?.remate===remate?this._rosganNameStrapLast:{name:'',role:''};
17695    return {rosNameOnAir:!!person,rosNameHidden:!person,rosNameStrapClass:(person?'is-on ':'')+(copy.name.length>32?'is-long':''),rosNameOnAirText:copy.name,rosRoleOnAirText:copy.role};
17696  }
17697  setRosganTvConfig(patch = {}) {
17698    const current = this.loadRosganOpsState();
17699    const lifted = cert => String(current.commercial?.[cert]?.status || '').toLowerCase() === 'levantado';
17700    if (patch.selectedCert && lifted(patch.selectedCert)) return;
17701    if (Array.isArray(patch.extraSlots)) {
17702      if (patch.extraSlots.some((cert,index) => cert && lifted(cert) && current.tv.extraSlots?.[index] !== cert)) return;
17703      patch = {...patch,extraSlots:patch.extraSlots.map(cert => lifted(cert) ?'' :cert)};
17704    }
17705    if (Object.prototype.hasOwnProperty.call(patch, 'selectedCert')) {
17706      patch = { ...patch, extraSlots:[0,1,2].map(index => (current.tv.extraSlots?.[index] === patch.selectedCert || lifted(current.tv.extraSlots?.[index])) ?'' :String(current.tv.extraSlots?.[index] || '')) };
17707    }
17708    if(Object.keys(patch).every(key=>
17708JSON.stringify(current.tv[key])===JSON.stringify(patch[key])))return;
17709    current.tv = { ...current.tv, ...patch };
17710    this.saveRosganOpsState(current);
17711    if (Object.prototype.hasOwnProperty.call(patch, 'selectedCert')) setTimeout(() => this.ensureRosganOpsSelectedVideo(), 0);
17712  }
17713  async copyRosganVmixUrl(){
17714    try{
17715      const id=this.remateRouteId(this.state.currentRemate);
17716      const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/airtime');
17717      const url=new URL('/remates/'+encodeURIComponent(id)+'/tv','https://rosgan.app');
17718      url.searchParams.set('airtime',result.data.outputId);
17719      await navigator.clipboard.writeText(url.href);
17720      this.showToast('URL vinculada copiada. Pegala en el Browser de vMix cuando esté fuera del aire.');
17721    }catch(error){this.showToast('No se pudo copiar la URL de vMix. '+error.message);}
17722  }
17723  editRosganVmixConnection(field,value){
17724    const id=this.remateRouteId(this.state.currentRemate),tv=this.loadRosganOpsState().tv;
17725    const draft=this.state.rosganVmixDraft?.id===id?this.state.rosganVmixDraft:{id,host:tv.vmixHost||'192.168.122.28:8088',input:tv.vmixBrowserInputNumber||''};
17726    this.setState({rosganVmixDraft:{...draft,[field]:value}});
17727  }
17728  saveRosganVmixConnection(){
17729    const id=this.remateRouteId(this.state.currentRemate),tv=this.loadRosganOpsState().tv;
17730    const draft=this.state.rosganVmixDraft?.id===id?this.state.rosganVmixDraft:{host:tv.vmixHost||'192.168.122.28:8088',input:tv.vmixBrowserInputNumber};
17731    try{
17732      const raw=String(draft.host).trim().replace(/^https?:\/\//i,'').replace(/\/$/,'');
17733      if(!/^[a-z0-9.-]+(?::\d{1,5})?$/i.test(raw))throw Error('Ingresá una IP o nombre de equipo válido.');
17734      const url=new URL('http://'+raw),input=Number(draft.input);
17735      if(!Number.isInteger(input)||input<1||input>1000)throw Error('La entrada debe ser un número entre 1 y 1000.');
17736      const host=url.hostname+':'+(url.port||'8088');
17737      this.setRosganTvConfig({vmixHost:host,vmixBrowserInputNumber:input,vmixBrowserInputKey:host!==(tv.vmixHost||'192.168.122.28:8088')||input!==Number(tv.vmixBrowserInputNumber)?'':tv.vmixBrowserInputKey||''});
17738      this.setState({rosganVmixDraft:null});
17739    }catch(error){this.showToast(error.message);}
17740  }
17741  selectRosganTvLot(event) { this.setRosganTvConfig({ selectedCert: String(event?.target?.value || '') }); }
17742  rosganTvPane() {
17743    const slot = Number(new URLSearchParams(window.location?.search || '').get('tvSlot'));
17744    return Number.isInteger(slot) && slot >= 1 && slot <= 4 ?slot :0;
17745  }
17746  rosganTvSelectedCert(tv = this.state.rosganOpsConfig, lots = this.state.remateLots || []) {
17747    const slot = this.rosganTvPane();
17748    return String((slot > 1 ?tv?.extraSlots?.[slot - 2] :tv?.selectedCert) || this.remateLotRouteId(lots[0]) || '');
17749  }
17750  rosganTvSlots(tv = this.state.rosganOpsConfig, lots = this.state.remateLots || []) {
17751    if (this.rosganTvPane()) return [];
17752    const certs = [String(tv?.selectedCert || this.remateLotRouteId(lots[0]) || ''), ...(Array.isArray(tv?.extraSlots) ?tv.extraSlots.slice(0,3) :[])];
17753    return certs.flatMap((cert,index) => cert && certs.indexOf(cert) === index && lots.some(lot => this.remateLotRouteId(lot) === cert) ?[{ slot:index + 1, cert }] :[]);
17754  }
17755  toggleRosganTvSlot(cert, slot) {
17756    if (![2,3,4].includes(slot)) return;
17757    if (String(this.loadRosganOpsState().commercial?.[cert]?.status || '').toLowerCase() === 'levantado') return;
17758    const tv = this.loadRosganOpsState().tv;
17759    const primary = String(tv.selectedCert || this.remateLotRouteId(this.state.remateLots?.[0]) || '');
17760    if (cert === primary) return;
17761    const slots = [0,1,2].map(index => String(tv.extraSlots?.[index] || ''));
17762    const remove = slots[slot - 2] === cert;
17763    const extraSlots = slots.map(value => value === cert ?'' :value);
17764    extraSlots[slot - 2] = remove ?'' :cert;
17765    this.setRosganTvConfig({ extraSlots });
17766  }
17767  updateRosganTvField(cert, name, value) {
17768    const remate = this.state.currentRemate;
17769    const key = this.remateRouteId(remate) + '/' + cert;
17770    this._rosganTvEdits ||= {};
17771    const entry = this._rosganTvEdits[key] ||= { remate, cert, changes: {}, status: '' };
17772    entry.revision = (entry.revision || 0) + 1;
17773    entry.touched ||= {};
17774    entry.touched[name] = true;
17775    entry.changes[name] = value;
17776    entry.status = 'Cambios pendientes. Presioná guardar para actualizar TV.';
17777    this.setState((s) => ({ rosganOpsPulse: s.rosganOpsPulse + 1 }));
17778  }
17779  publishRosganTvPreview(entry, confirmedLot = null, failed = false) {
17780    try {
17781      // Only confirmed data goes on air; newer edits remain private drafts until the next save.
17782      const changes = {};
17783      const packet = { remateId: this.remateRouteId(entry.remate), cert
17783: entry.cert, changes, confirmedLot, at: Date.now(),
17784        categories: (this.state.rosganLotCategories || []).map(({ id, name }) => ({ id, name })),
17785        breeds: (this.state.rosganLotBreeds || []).map(({ id, name }) => ({ id, name })) };
17786      localStorage.setItem(this.rosganOpsStorageKey(entry.remate) + ':lot-preview', JSON.stringify(packet));
17787    } catch { /* Incomplete numeric input stays in the editor until it is valid. */ }
17788  }
17789  receiveRosganTvPreview(value) {
17790    if (this.state.route !== 'rosganTvLower') return;
17791    try {
17792      const packet = JSON.parse(value || 'null');
17793      if (!packet?.cert || packet.remateId !== this.remateRouteId(this.state.currentRemate) || Date.now() - packet.at > 30000) return;
17794      packet.changes = window.RosganTvEditor.serializeChanges(packet.changes);
17795      this._rosganTvPreviews ||= {};
17796      this._rosganTvPreviews[packet.remateId + '/' + packet.cert] = packet;
17797      // Discard reads started before this edit/confirmation.
17798      const key = this.remateRouteId(this.state.currentRemate) + '/' + packet.cert;
17799      if (this._rosganTvPendingReads) delete this._rosganTvPendingReads[key];
17800      this.setState(s => ({
17801        ...(packet.confirmedLot ?{ remateLots: (s.remateLots || []).map(lot => this.remateLotRouteId(lot) === packet.cert ?this.mergeRosganTvConfirmedLot(lot, packet.confirmedLot) : lot) } : {}),
17802        rosganOpsPulse: Number(s.rosganOpsPulse || 0) + 1,
17803      }));
17804    } catch { /* Ignore malformed or expired preview messages. */ }
17805  }
17806  rosganTvPreviewLot(lot) {
17807    const packet = this._rosganTvPreviews?.[this.remateRouteId(this.state.currentRemate) + '/' + this.remateLotRouteId(lot)];
17808    if (!packet || Date.now() - packet.at > 30000 || !Object.keys(packet.changes).length) return lot;
17809    return this.normalizeRosganActiveLot(window.RosganTvEditor.previewLot(lot, packet.changes, packet.categories, packet.breeds), 0, this.state.currentRemate);
17810  }
17811  async saveRosganTvLotEdits(entry) {
17812    if (entry.saving || !Object.keys(entry.changes).length) return;
17813    entry.saving = true;
17814    entry.status = 'Guardando en el lote…';
17815    const changes = entry.changes;
17816    entry.inFlight = changes;
17817    entry.changes = {};
17818    clearTimeout(entry.timer);
17819    this.setState((s) => ({ rosganOpsPulse: s.rosganOpsPulse + 1 }));
17820    try {
17821      const payload = { changes: window.RosganTvEditor.serializeChanges(changes) };
17822      const saved = await this.apiFetch('/api/remate-operations/' + encodeURIComponent(this.remateRouteId(entry.remate)) + '/lots/' + encodeURIComponent(entry.cert), { method: 'PUT', body: JSON.stringify(payload) });
17823      const refreshed = saved?.data?.lot || await this.apiFetch('/api/remate-operations/' + encodeURIComponent(this.remateRouteId(entry.remate)) + '/lots/' + encodeURIComponent(entry.cert), { cache: 'no-store' });
17824      const lot = this.normalizeRosganActiveLot(refreshed?.data || refreshed, 0, entry.remate);
17825      // Install the confirmed lot before publishing its version to other views.
17826      if (this.remateRouteId(this.state.currentRemate) === this.remateRouteId(entry.remate)) {
17827        this.setState((s) => ({ remateLots: (s.remateLots || []).map((item) => this.remateLotRouteId(item) === entry.cert ?this.mergeRosganTvConfirmedLot(item, lot) : item) }));
17828      }
17829      // Also notify on a no-op retry: an earlier write may have succeeded while its response was lost.
17830      {
17831        const current = this.loadRosganOpsState(entry.remate);
17832        const key = this.remateRouteId(entry.remate) + '/' + entry.cert;
17833        this._rosganTvSeenVersions ||= {};
17834        const version = Math.max(Date.now(), Number(current.tv.lotVersions?.[entry.cert] || 0) + 1, Number(this._rosganTvSeenVersions[key] || 0) + 1);
17835        this._rosganTvSeenVersions[key] = version;
17836        current.tv.lotVersions = { ...current.tv.lotVersions, [entry.cert]: version };
17837        this.saveRosganOpsState(current, entry.remate);
17838      }
17839      entry.status = Object.keys(entry.changes).length ?'Cambios pendientes. Presioná guardar para actualizar TV.' :'Guardado en el lote y actualizado en TV';
17840      entry.inFlight = {};
17841      this.publishRosganTvPreview(entry, lot);
17842    } catch (error) {
17843      entry.changes = { ...changes, ...entry.changes };
17844      entry.status = 'No guardado: ' + (error?.message || 'Error de conexión.');
17845      entry.failed = true;
17846      this.publishRosganTvPreview(entry, null, true);
17847      this.showToast(entry.status);
17848    } finally {
17849      entry.saving = false;
17850      entry.inFlight = {};
17851      this.setState((s) => ({ rosganOpsPulse: s.rosganOpsPulse + 1 }));
17852      entry.failed = false;
17853    }
17854  }
17855  async syncRosganTvLotVersions(tv, remate = this.state.currentRemate) {
17856    this._rosganTvSeenVersions ||= {};
17857    this._rosganTvPendingReads ||= {};
17858    for (const [cert, version] of Object.entries(tv?.lotVersions || {})) {
17859      const key = this.remateRouteId(remate) + '/' + cert;
17860      // Do not consume versions before the initial catalog has installed the lot.
17861      if (!(this.state.remateLots || []).some(item => this.remateLotRouteId(item) === cert)) continue;
17862      if (Number(this._rosganTvSeenVersions[key] || 0) >= Number(version)) continue;
17863      const edit = this._rosganTvEdits?.[key];
17864      if (edit?.saving || Object.keys(edit?.changes || {}).length) continue;
17865      if (this._rosganTvPendingReads[key]?.version === version) continue;
17866      const request = { version, revision: edit?.revision || 0 };
17867      this._rosganTvPendingReads[key] = request;
17868      try {
17869        const response = await this.apiFetch('/api/remate-operations/' + encodeURIComponent(this.remateRouteId(remate)) + '/lots/' + encodeURIComponent(cert), { cache: 'no-store' });
17870        if (this.remateRouteId(remate) !== this.remateRouteId(this.state.currentRemate)) continue;
17871        const latestEdit = this._rosganTvEdits?.[key];
17872        // Never apply a read started before a newer edit, save or version request.
17873        if (this._rosganTvPendingReads[key] !== request || (latestEdit?.revision || 0) !== request.revision || latestEdit?.saving || Object.keys(latestEdit?.changes || {}).length || Number(this._rosganTvSeenVersions[key] || 0) >= Number(version)) continue;
17874        const lot = this.normalizeRosganActiveLot(response?.data || response, 0, remate);
17875        this._rosganTvSeenVersions[key] = version;
17876        this.setState((s) => ({ remateLots: (s.remateLots || []).map((item) => this.remateLotRouteId(item) === cert ?this.mergeRosganTvConfirmedLot(item, lot) : item) }));
17877      } catch { /* Keep the last confirmed version; the next poll can retry. */ }
17878      finally { if (this._rosganTvPendingReads[key] === request) delete this._rosganTvPendingReads[key]; }
17879    }
17880  }
17881  mergeRosganTvConfirmedLot(item, lot) {
17882    // The TV editor cannot change media. Its direct DB projection does not resolve video URLs.
17883    return { ...item, ...lot, videoUrl: item.videoUrl || lot.videoUrl || '', video_uri: item.video_uri || lot.video_uri || '', thumbnailUrl: item.thumbnailUrl || lot.thumbnailUrl || '' };
17884  }
17885  rosganTvEditorFields(lot) {
17886    if (!lot) return [];
17887    const cert = this.remateLotRouteId(lot);
17888    const category = (this.state.rosganLotCategories || []).find((item) => String(item.id) === String(lot.categoryId));
17889    const quantityMode = category?.quantityMode || (lot.maleQuantity != null || lot.femaleQuantity != null ?'male_female' : lot.motherQuantity != null || lot.calfQuantity != null ?'mothers_calves' : 'total');
17890    const fields = [
17891      ['categoryId','Categoría','category'], ['breedIds','Razas','breeds'],
17892      ['quantity','Cabezas','number'], ['maleQuantity','Machos','number'], ['femaleQuantity','Hembras','number'], ['motherQuantity','Madres','number'], ['calfQuantity','Crías','number'],
17893      ['locality','Localidad'], ['province','Provincia'], ['paymentTerm','Plazo de pago'],
17894      ['animalClass','Clase'], ['ageValue','Edad','number'], ['ageUnit','Unidad de edad'], ['bodyCondition','Estado'], ['lotDescriptions','Descripción (una por línea)','array'], ['dentition','Dentición'],
17895      ['reproductiveStatus','Estado reproductivo'], ['destination','Destino'],
17896      ['nutrition','Alimentación'], ['health','Sanidad'], ['tickStatus','Garrapata'],
17897      ['hasMioMio','Mío mío','boolean'], ['isAptoChina','Apto China','boolean'], ['isTermino','Término','boolean'],
17898      ...(lot.invernada ?[['invernada.averageWeight','Peso promedio (kg)','number'], ['invernada.minWeight','Peso mínimo (kg)','number'], ['invernada.maxWeight','Peso máximo (kg)','number'], ['invernada.weighing','Pesaje'], ['invernada.desbaste','Desbaste'], ['invernada.hasLiquidBrand','Marca líquida','boolean'], ['invernada.isAptoVisec','Apto Visec','boolean']] : []),
17899      ...(lot.vientres ?[['vientres.service','Servicio'], ['vientres.pregnancy','Preñez'], ['vientres.calving','Parición']] : []),
17900      ...(lot.faena ?[['faena.subCategory','Subcategoría faena'], ['faena.averageWeight','Peso promedio (kg)','number'], ['faena.minWeight','Peso mínimo (kg)','number'], ['faena.maxWeight','Peso máximo (kg)','number'], ['faena.weighing','Pesaje'], ['faena.desbaste','Desbaste'], ['faena.conditionType','Condición faena'], ['faena.deliveryType','Entrega'], ['faena.finishing','Terminación'], ['faena.estimatedYield','Rinde','number']] : []),
17901      ['observations','Observaciones (roll sobre el zócalo)','textarea'],
17902    ];
17903    return fields.filter(([name]) => name === 'quantity' ?quantityMode === 'total' : ['maleQuantity','femaleQuantity'].includes(name) ?quantityMode === 'male_female' : ['motherQuantity','calfQuantity'].includes(name) ?quantityMode === 'mothers_calves' : true).map(([name,label,type = 'text']) => {
17904      const editState = this._rosganTvEdits?.[this.remateRouteId(this.state.currentRemate) + '/' + cert];
17905      const edits = { ...editState?.inFlight, ...editState?.changes };
17906      const raw = Object.prototype.hasOwnProperty.call(edits, name) ?edits[name] : name.split('.').reduce((obj,key) => obj?.[key], lot) ?? '';
17907      const options = type === 'category' ?(this.state.rosganLotCategories || []).map((item) => ({ value: String(item.id), label: item.name })) : [];
17908      const breedOptions = (this.state.rosganLotBreeds || []).map((item) => ({ label: item.name, selectedClass: Array.isArray(raw) && raw.includes(String(item.id)) ?'is-selected' : '', toggle: () => {
17909        const values = Array.isArray(raw) ?raw : [];
17910        this.updateRosganTvField(cert,name,values.includes(String(item.id)) ?values.filter((v) => v !== String(item.id)) : [...values,String(item.id)]);
17911      } }));
17912      const hasValue = (value) => {
17913        if (typeof value === 'boolean') return true;
17914        if (value == null) return false;
17915        if (Array.isArray(value)) return value.some(hasValue);
17916        const text = String(value).trim();
17917        if (!text || /^(?:\?|[-—–]|null|undefined|sin datos?)$/i.test(text)) return false;
17918        return type !== 'number' || (Number.isFinite(Number(text)) && Number(text) > 0);
17919      };
17920      // Keep a field available while its clear/replacement is being saved.
17921      const visible = hasValue(raw) || Object.prototype.hasOwnProperty.call(edits, name) || !!editState?.touched?.[name];
17922      return { key: cert + '/' + name, name, label, type, options, breedOptions, visible, value: type === 'boolean' ?(raw === true ?'true' : raw === false ?'false' : '') : Array.isArray(raw) ?raw.join('\n') : String(raw),
17923        nullableBoolean: name === 'hasMioMio', isBoolean: type === 'boolean', isSelect: type === 'category', isBreeds: type === 'breeds', isTextarea: ['textarea','array'].includes(type), isInput: !['boolean','textarea','array','category','breeds'].includes(type),
17924        change: (event) => this.updateRosganTvField(cert, name, type === 'boolean' ?(event.target.value === '' ?null : event.target.value === 'true') : type === 'array' ?event.target.value.split('\n') : event.target.value) };
17925    }).filter((field) => field.visible);
17926  }
17927  stepRosganTvLot(delta = 1) {
17928    const lots = (this.state.remateLots || []).slice().sort((a, b) => this.compareRosganSalesOrder(a,b));
17929    if (!lots.length) return;
17930    const selected = String(this.state.rosganOpsConfig?.selectedCert || '');
17931    let index = Math.max(0, lots.findIndex((lot) => this.remateLotRouteId(lot) === selected));
17932    const commercial = this.loadRosganOpsState().commercial || {};
17933    for (let step = 0; step < lots.length; step++) {
17934      index = (index + Number(delta || 0) + lots.length) % lots.length;
17935      const cert = this.remateLotRouteId(lots[index]);
17936      if (String(commercial[cert]?.status || '').toLowerCase() !== 'levantado') {
17937        this.setRosganTvConfig({ selectedCert: cert });
17938        return;
17939      }
17940    }
17941  }
17942  playRosganTvVideo(event) {
17943    const video = event?.currentTarget;
17944    if (!video || video !== this._rosganTvDecks?.active) return;
17945    video.muted = true;
17946    video.loop = true;
17947    if (video.ended) video.currentTime = 0;
17948    video.play?.().catch(() => {});
17949  }
17950  resetRosganTvDecks() {
17951    if (!this._rosganTvDecks) return;
17952    this._rosganTvDecks.entries.forEach((entry) => entry.cancel?.());
17953    this._rosganTvDecks.players.forEach((video) => {
17954      video.classList.remove('is-on-air');
17955      video.pause();
17956      video.removeAttribute('src');
17957      video.load();
17958    });
17959    this._rosganTvDecks = null;
17960  }
17961  rosganPriceCategory(category, breed) {
17962    const normalize = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
17963    const key = normalize(category);
17964    const dairy = /holando|kiwi/.test(normalize(breed) + key);
17965    if (key.includes('prenez')) return /^(vaq|vaquillona)/.test(key) ? 'Vaq. c/gtía. de preñez' : key.startsWith('vaca') ? 'Vacas c/gtía. de preñez' : '';
17966    if (key.startsWith('vaca') && key.includes('cria')) return 'Vacas con cría al pie';
17967    if (key.startsWith('vaca') && /inv/.test(key)) return 'Vacas de invernada';
17968    if (key.startsWith('ternero')) return /ternerosas/.test(key) ? (dairy ? 'Terneros/as Holando' : 'Terneros/as') : (dairy ? 'Terneros Holando' : 'Terneros');
17969    if (key.startsWith('ternera')) return dairy ? 'Terneras Holando' : 'Terneras';
17970    if (key.includes('novillito') && key.includes('vaquillona')) return 'Novillitos y Vaquillonas';
17971    if (key.startsWith('toro')) return 'Toros';
17972    if (key.includes('vientre') && dairy) return 'Vientres Holando';
17973    if (key.includes('servicio') && dairy) return 'Vaq. para servicio Holando';
17974    if (key.startsWith('novillito')) return dairy ? 'Novillos Holando' : 'Novillitos';
17975    if (key.startsWith('novillo')) return dairy ? 'Novillos Holando' : 'Novillos';
17976    if (key.startsWith('vaquillona')) return 'Vaquillonas';
17977    return '';
17978  }
17979  rosganTvExtraFields(lot) {
17980    if (!lot) return [];
17981    const numeric = (value, unit = '') => Number(value) >
17981 0 ?Number(value).toLocaleString('es-AR') + unit : '';
17982    const yesNo = (value) => typeof value === 'boolean' ?(value ?'Sí' : 'No') : '';
17983    // Explicit public fields: never expose raw API keys, owners or internal metadata on air.
17984    const fields = [
17985      ['PESO MÍNIMO', numeric(lot.peso_minimo, ' kg')], ['PESO MÁXIMO', numeric(lot.peso_maximo, ' kg')],
17986      ['CLASE', lot.clase], ['EDAD', lot.edad], ['ESTADO', lot.estado],
17987      ['DESCRIPCIÓN', lot.descripcion_lote], ['DENTICIÓN', lot.denticion],
17988      ['ESTADO REPRODUCTIVO', lot.estado_reproductivo], ['DESTINO', lot.destino],
17989      ['PESAJE', lot.pesaje], ['DESBASTE', lot.desbaste], ['ALIMENTACIÓN', lot.pastoreo],
17990      ['SANIDAD', lot.sanidad], ['MÍO MÍO', yesNo(lot.hasMioMio)],
17991      ['MARCA LÍQUIDA', yesNo(lot.invernada?.hasLiquidBrand)],
17992      ['SERVICIO', lot.servicio],
17993      ['PREÑEZ', this.rosganTvFixedPregnancy(lot) ? '' : lot.prenez], ['PARICIÓN', lot.paricion], ['SUBCATEGORÍA', lot.faena_subcategoria],
17994      ['CONDICIÓN DE VENTA', lot.faena_condicion], ['ENTREGA', lot.faena_entrega],
17995      ['TERMINACIÓN', lot.faena_terminacion], ['RINDE ESTIMADO', lot.faena_rinde],
17996    ];
17997    return fields.flatMap(([label, raw]) => {
17998      if (raw == null || typeof raw === 'object') return [];
17999      const text = String(raw).replace(/\s+/g, ' ').trim();
18000      if (!text || /^(?:\?|[-—–]|null|undefined|n\/a|sin datos?)$/i.test(text)) return [];
18001      // Split long values into readable slides instead of clipping the information.
18002      const pages = [];
18003      let rest = text;
18004      while (rest.length > 48) {
18005        const space = rest.lastIndexOf(' ', 48);
18006        const end = space > 20 ?space : 48;
18007        pages.push(rest.slice(0, end));
18008        rest = rest.slice(end).trim();
18009      }
18010      if (rest) pages.push(rest);
18011      return pages.map((value) => ({ label, value }));
18012    });
18013  }
18014  stageRosganTvVideo(lot) {
18015    const players = Array.from(document.querySelectorAll('video[data-rosgan-tv-deck]'));
18016    if (players.length !== 5) return;
18017    if (this._rosganTvDecks?.players[0] !== players[0]) {
18018      this.resetRosganTvDecks();
18019      this._rosganTvDecks = { players, entries: new Map(), active: null, onAirCert: '', desiredCert: '', warmCerts: new Set() };
18020    }
18021    const decks = this._rosganTvDecks;
18022    const cert = this.remateLotRouteId(lot);
18023    const url = lot.videoUrl || lot.video_uri || '';
18024    const key = cert + '|' + url;
18025    decks.desiredCert = cert;
18026    decks.desiredKey = key;
18027    const onAir = decks.entries.get(decks.active);
18028    if (onAir && globalThis.RosganTvHealth?.stalled(onAir)) {
18029      onAir.unhealthy = true;
18030      decks.onAirKey = '';
18031    }
18032    if (decks.onAirKey === key) {
18033      const active = decks.active;
18034      active.classList.add('is-on-air');
18035      active.loop = true;
18036      active.muted = true;
18037      if (active.ended) active.currentTime = 0;
18038      if (active.paused) active.play().catch(() => {});
18039    } else if (url) {
18040      this.prepareRosganTvDeck(lot, true);
18041    }
18042    // The on-air video has priority over speculative network requests.
18043    if (decks.onAirCert === cert) this.prewarmRosganTvNeighbors(lot);
18044  }
18045  prepareRosganTvDeck(lot, priority = false) {
18046    const decks = this._rosganTvDecks;
18047    if (!decks || this.state.route !== 'rosganTvLower') return;
18048    const cert = this.remateLotRouteId(lot);
18049    const url = lot.videoUrl || lot.video_uri || '';
18050    if (!url) return;
18051    const key = cert + '|' + url;
18052    let entry = Array.from(decks.entries.values()).find((item) => item.key === key && !item.unhealthy);
18053    if (entry) {
18054      if (entry.ready && priority) this.activateRosganTvDeck(entry);
18055      if (!entry.failedAt || Date.now() - entry.failedAt < 15000) return;
18056    }
18057    const next = entry?.video || decks.players.find((video) => !decks.entries.has(video)) ||
18058      decks.players.find((video) => video !== decks.active && !decks.warmCerts.has(decks.entries.get(video)?.cert) && decks.entries.get(video)?.cert !== decks.desiredCert) ||
18059      (priority ?decks.players.find((video) => video !== decks.active && decks.entries.get(video)?.cert !== decks.desiredCert) 
18059: null);
18060    if (!next) return;
18061    decks.entries.get(next)?.cancel?.();
18062    entry = { key, cert, video: next, ready: false, starting: false, failedAt: 0 };
18063    decks.entries.set(next, entry);
18064    let frameId = null;
18065    let cancelled = false;
18066    let preparationTimer = null;
18067    const cleanup = () => {
18068      clearTimeout(preparationTimer);
18069      next.removeEventListener('canplay', ready);
18070      next.removeEventListener('error', failed);
18071      if (frameId !== null) next.cancelVideoFrameCallback?.(frameId);
18072    };
18073    const isCurrent = () => !cancelled && this._rosganTvDecks === decks && decks.entries.get(next) === entry && this.state.route === 'rosganTvLower';
18074    const decoded = () => {
18075      if (!isCurrent()) return;
18076      cleanup();
18077      entry.ready = true;
18078      next.pause();
18079      next.dataset.tvPrepared = cert;
18080      if (decks.desiredKey === key) this.activateRosganTvDeck(entry);
18081    };
18082    const failed = () => {
18083      if (!isCurrent()) return;
18084      cleanup();
18085      cancelled = true;
18086      next.pause();
18087      entry.failedAt = Date.now();
18088    };
18089    const ready = async () => {
18090      if (entry.starting || !isCurrent() || next.readyState < 3) return;
18091      entry.starting = true;
18092      try {
18093        await next.play();
18094        if (!isCurrent()) return;
18095        if (next.requestVideoFrameCallback) frameId = next.requestVideoFrameCallback(decoded);
18096        else requestAnimationFrame(() => requestAnimationFrame(decoded));
18097      } catch { failed(); }
18098    };
18099    entry.cancel = () => { cancelled = true; cleanup(); next.pause(); };
18100    next.classList.remove('is-on-air');
18101    delete next.dataset.tvPrepared;
18102    next.muted = true;
18103    next.loop = true;
18104    next.playsInline = true;
18105    next.preload = 'auto';
18106    next.addEventListener('canplay', ready);
18107    next.addEventListener('error', failed);
18108    preparationTimer = setTimeout(() => { if (isCurrent()) failed(); }, 20000);
18109    preparationTimer.unref?.();
18110    next.src = url;
18111    next.load();
18112  }
18113  activateRosganTvDeck(entry) {
18114    const decks = this._rosganTvDecks;
18115    if (!decks || !entry.ready || entry.activating || decks.desiredKey !== entry.key || entry.unhealthy || decks.active === entry.video) return;
18116    const next = entry.video;
18117    entry.activating = true;
18118    // The hidden player already owns a decoded frame. No src/load/seek at take time.
18119    (globalThis.RosganTvHealth ? globalThis.RosganTvHealth.play(next) : next.play()).then(() => {
18120      if (this._rosganTvDecks !== decks || decks.desiredKey !== entry.key || entry.unhealthy || decks.entries.get(next) !== entry || this.state.route !== 'rosganTvLower') {
18121        if (decks.entries.get(next) === entry && decks.active !== next) next.pause();
18122        return;
18123      }
18124      if (decks.active === next) return;
18125      const previous = decks.active;
18126      next.classList.add('is-on-air');
18127      decks.active = next;
18128      decks.onAirKey = entry.key;
18129      decks.onAirCert = entry.cert;
18130      entry.health = null;
18131      previous?.classList.remove('is-on-air');
18132      previous?.pause();
18133      this.setState({ rosganOpsPulse: Date.now(), rosganTvFieldStep: 0 });
18134      const lot = this.state.remateLots?.find((item) => this.remateLotRouteId(item) === entry.cert);
18135      if (lot) this.prewarmRosganTvNeighbors(lot);
18136    }).catch(() => { entry.ready = false; entry.failedAt = Date.now(); }).finally(() => { entry.activating = false; });
18137  }
18138  prewarmRosganTvNeighbors(lot) {
18139    if (this.rosganTvPane?.()) return;
18140    if (this.rosganTvSlots?.().length > 1) return;
18141    const decks = this._rosganTvDecks;
18142    if (!decks) return;
18143    const lots = (this.state.remateLots || []).slice().sort((a, b) => this.compareRosganSalesOrder(a,b));
18144    const index = lots.findIndex((item) => this.remateLotRouteId(item) === this.remateLotRouteId(lot));
18145    if (index < 0 || lots.length < 2) return;
18146    const neighbors = [...new Set([lots[(index + 1) % lots.length], lots[(index + 2) % lots.length], lots[(index - 1 + lots.length) % lots.length]])].filter((item) => item !== lots[index]);
18147    decks.warmCerts = new Set(neighbors.map((item) => this.remateLotRouteId(item)));
18148    neighbors.forEach((item) => {
18149      if (item.videoUrl || item.video_uri) this.prepareRosganTvDeck(item);
18150      else this.resolveRosganTvVideo(item);
18151    });
18152  }
18153  async ensureRosganOpsSelectedVideo() {
18154    if (this.state.route !== 'rosganTvLower') return;
18155    const lots = this.state.remateLots || [];
18156    const selectedCert = this.rosganTvSelectedCert(this.state.rosganOpsConfig, lots);
18157    const lot = lots.find((item) => this.remateLotRouteId(item) === selectedCert) || lots[0];
18158    if (!lot) return;
18159    const slots = this.rosganTvSlots();
18160    if (slots.length > 1) {
18161      this.stageRosganTvMultiview(slots);
18162      // Hold the existing output while all requested videos decode off air.
18163      if (!this._rosganTvDecks?.active && !this._rosganTvAirSlots?.length) this.stageRosganTvVideo(lot);
18164      if (!lot.videoUrl && !lot.video_uri) this.resolveRosganTvVideo(lot);
18165      return;
18166    }
18167    this.stageRosganTvVideo(lot);
18168    if (this._rosganTvAirSlots?.length && this._rosganTvDecks?.onAirKey === selectedCert + '|' + (lot.videoUrl || lot.video_uri || '')) {
18169      this._rosganTvMultiview?.hide();this._rosganTvAirSlots=[];this.setState({rosganOpsPulse:Date.now()});
18170    }
18171    if (!lot.videoUrl && !lot.video_uri) this.resolveRosganTvVideo(lot);
18172  }
18173  resetRosganTvMultiview() {
18174    this._rosganTvMultiview?.dispose();this._rosganTvMultiview=null;this._rosganTvAirSlots=[];
18175  }
18176  rosganTvTemporaryConsignataria(remate, lot) {
18177    if (String(this.remateRouteId(remate)) !== '6' || !lot) return null;
18178    const comitente = lot.comitente && typeof lot.comitente === 'object' ?lot.comitente : {};
18179    const establishment = lot.establishment || lot.establecimiento || {};
18180    const values = [
18181      lot.comitenteAlias, lot.comitenteNombreFiscal, lot.comitenteFiscalName, lot.comitenteNombre, lot.nombre_comitente, lot.comitenteName,
18182      typeof lot.comitente === 'string' ?lot.comitente : '',
18183      comitente.alias, comitente.nombreFiscal, comitente.razonSocial, comitente.fiscalName, comitente.name, comitente.nombre,
18184      establishment.comitenteAlias, establishment.comitenteNombreFiscal, establishment.comitenteNombre,
18185      establishment.comitente?.alias, establishment.comitente?.nombreFiscal, establishment.comitente?.name,
18186    ];
18187    if (!values.some((value) => this.consKey(value).includes('consignacionescordoba'))) return null;
18188    return {
18189      name: 'Consignaciones Córdoba S.A.',
18190      logo: '/assets/consignatarias-white/consignaciones-cordoba-s-a.png',
18191      color: '#5985ad',
18192    };
18193  }
18194  stageRosganTvMultiview(slots) {
18195    const players=Array.from(document.querySelectorAll('video[data-rosgan-tv-multi-deck]'));
18196    if(players.length!==8) return;
18197    if(this._rosganTvMultiview?.players[0]!==players[0]) {
18198      this.resetRosganTvMultiview();
18199      this._rosganTvMultiview=new window.RosganTvMultiview.Multiview(players,airSlots=>{
18200        if(this.state.route!=='rosganTvLower' || this.rosganTvSlots().length<2) return;
18201        this._rosganTvAirSlots=airSlots;
18202        this._rosganTvDecks?.active?.pause();
18203        this.setState({rosganOpsPulse:Date.now(),rosganTvFieldStep:0});
18204      });
18205    }
18206    const prepared=slots.map(slot=>{
18207      const lot=this.state.remateLots.find(lot=>this.remateLotRouteId(lot)===slot.cert);
18208      if(!lot.videoUrl&&!lot.video_uri)this.resolveRosganTvVideo(lot);
18209      return {...slot,url:lot.videoUrl||lot.video_uri||''};
18210    });
18211    this._rosganTvMultiview.stage(prepared);
18212  }
18213  rosganTvIsAptoChina(lot){
18214    const value=lot?.isAptoChina??lot?.aptoChina??lot?.apto_china;
18215    return value===true||value===1||['true','1','si','sí'].includes(String(value??'').trim().toLowerCase());
18216  }
18217  rosganTvIsAptoVisec(lot) {
18218    const value = lot?.invernada?.isAptoVisec ?? lot?.isAptoVisec ?? lot?.aptoVisec;
18219    return value === true || value === 1 || ['true','1','si','sí'].includes(String(value ?? '').trim().toLowerCase());
18220  }
18221  rosganTvCategory(lot) {
18222    const category = lot?.nombre_categoria || 'Lote';
18223    const term = lot?.isTermino === true || lot?.isTermino === 1 || ['true','1','si','sí'].includes(String(lot?.isTermino ?? '').toLowerCase());
18224    return term && !/t[eé]rmino/i.test(category) ? category + ' a término' : category;
18225  }
18226  rosganTvQuantity(lot) {
18227    return Number(lot?.cantidad) > 0 ? Number(lot.cantidad).toLocaleString('es-AR') : '';
18228  }
18229  rosganTvQuantityBreakdown(lot) {
18230    const number = value => value != null && value !== '' && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value).toLocaleString('es-AR') : '—';
18231    if (Number(lot?.cantidad_madres) > 0 || Number(lot?.cantidad_crias) > 0) return number(lot.cantidad_madres) + ' M\n' + number(lot.cantidad_crias) + ' C';
18232    if (Number(lot?.cantidad_machos) > 0 || Number(lot?.cantidad_hembras) > 0) return number(lot.cantidad_machos) + ' M\n' + number(lot.cantidad_hembras) + ' H';
18233    return '';
18234  }
18235  rosganTvFixedPregnancy(lot) {
18236    const name = String(lot?.nombre_categoria || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
18237    const guaranteed = /(?:c\s*\/\s*|con\s+)(?:gtia\.?|garantia)(?:\s+de)?\s+prenez/.test(name);
18238    return guaranteed ? String(lot?.prenez || lot?.vientres?.pregnancy || 'Sin informar') : '';
18239  }
18240  rosganTvLotNumber(lot) {
18241    if (!lot) return '';
18242    const remate = this.state.currentRemate;
18243    const active = remate?.__rosganActive || lot.__rosganActiveLot;
18244    const publication = this.state.catalogPublication;
18245    if (active && (publication?.key !== this.catalogPublicationKey(remate) || publication?.salesOrderDefined !== true)) return '';
18246    const value = active ? lot.lotNumber : (lot.orden_final ?? lot.orden_venta ?? lot.lotNumber);
18247    return Number.isFinite(Number(value)) && Number(value) > 0 ? String(value) : '';
18248  }
18249  rosganTvAirPanes(tv) {
18250    return (this._rosganTvAirSlots || []).flatMap(slot=>{
18251      const lot=(this.state.remateLots || []).find(lot=>this.remateLotRouteId(lot)===slot.cert);
18252      if(!lot)return [];
18253      const temporaryCons=this.rosganTvTemporaryConsignataria(this.state.currentRemate,lot);
18254      const cons=temporaryCons?.name||this.consDisplayName(lot.nombre_consignatario)||'Rosgan';
18255      const logo=temporaryCons?.logo||this.consWhiteLogo(cons)||this.consLogo(cons);
18256      const key=this.consKey(cons);
18257      const record=(this.state.consignatarias||[]).find(item=>this.consKey(item.name||item.nombre)===key);
18258      const raw=record?.telefonos||record?.phones||record?.telefono||record?.phone||'';
18259      const contacts=tv.contactsByConsignataria?.[key]??(Array.isArray(raw)?raw.map(value=>typeof value==='string'?value:value.phone||value.telefono||'').filter(Boolean).join(' | '):String(raw));
18260      const extra=this.rosganTvExtraFields(lot),field=extra[(this.state.rosganTvFieldStep||0)%Math.max(1,extra.length)];
18261      const observations=String(lot.observations||lot.observaciones_certificado||lot.observaciones||'').replace(/\s+/g,' ').trim();
18262      const number=value=>Number(value)>0?Number(value).toLocaleString('es-AR'):'—';
18263      return [{style:'left:'+slot.x+'%;top:'+slot.y+'%;transform:scale('+slot.scale+')',
18264        LotNumber:this.rosganTvLotNumber(lot),Cert:slot.cert,Category:this.rosganTvCategory(lot),Breed:lot.raza||'',
18265        Quantity:this.rosganTvQuantity(lot),HasQuantity:!!this.rosganTvQuantity(lot),Pregn
18265ancy:this.rosganTvFixedPregnancy(lot),Weight:number(lot.peso_promedio)+' kg',HasWeight:Number(lot.peso_promedio)>0,
18266        QuantityFirst:this.rosganTvQuantityBreakdown(lot).split('\n')[0],QuantitySecond:this.rosganTvQuantityBreakdown(lot).split('\n')[1]||'',
18267        Location:[lot.nombre_localidad,lot.nombre_provincia].filter(Boolean).join(', '),Payment:lot.plazo||'',Tick:lot.garrapata||'',
18268        Consignataria:cons,ConsLogo:logo,ConsFullLogo:this.consFullHeroLogo(cons),HasConsLogo:!!logo,NoConsLogo:!logo,ConsInitial:cons.charAt(0),ConsColor:temporaryCons?.color||this.consBrandColor(cons),
18269        ShowChina:tv.visible!==false&&tv.lowerThirdVisible!==false&&this.rosganTvIsAptoChina(lot),
18270        ShowVisec:tv.visible!==false&&tv.lowerThirdVisible!==false&&this.rosganTvIsAptoVisec(lot),
18271        ShowBrandBug:tv.visible!==false&&tv.logosVisible!==false,ShowLower:tv.visible!==false&&tv.lowerThirdVisible!==false,
18272        Contacts:contacts,ShowContacts:tv.visible!==false&&tv.lowerThirdVisible!==false&&tv.contactsVisible!==false&&!!contacts,
18273        HasExtraFields:!!field,ExtraLabel:field?.label||'',ExtraValue:field?.value||'',Observations:observations,HasObservations:!!observations&&!/^(?:\?|[-—–]|null|sin datos?)$/i.test(observations),ObservationsDuration:Math.max(18,observations.length/8)}];
18274    });
18275  }
18276  async resolveRosganTvVideo(lot) {
18277    if (lot.videoUrl || lot.video_uri || lot.__videoResolving || Date.now() - (lot.__tvVideoRetryAt || 0) < 15000) return;
18278    const remateId = this.remateRouteId(this.state.currentRemate);
18279    const selectedCert = this.remateLotRouteId(lot);
18280    lot.__videoResolving = true;
18281    try {
18282      const response = await this.apiFetch(this.rosganLotVideosEndpoint(remateId, selectedCert), { cache: 'no-store' });
18283      if (this.remateRouteId(this.state.currentRemate) !== remateId) return;
18284      const videos = this.rosganArray(response?.videos || response);
18285      const primary = videos.find((item) => item?.url || item?.publicUrl || item?.videoUrl || item?.downloadUrl || item?.signedUrl);
18286      const url = primary?.url || primary?.publicUrl || primary?.videoUrl || primary?.downloadUrl || primary?.signedUrl || '';
18287      if (url) {
18288        this.setState({ remateLots: this.state.remateLots.map((item) => this.remateLotRouteId(item) === selectedCert ?{ ...item, videoUrl: url, video_uri: url, __videoResolving: false } : item) });
18289        this.ensureRosganOpsSelectedVideo();
18290      }
18291      else lot.__tvVideoRetryAt = Date.now();
18292    } catch { lot.__tvVideoRetryAt = Date.now(); }
18293    finally { lot.__videoResolving = false; }
18294  }
18295  remateLotAlertKey(remateId, lotCert) {
18296    return String(remateId || '') + '|' + String(lotCert || '');
18297  }
18298  async loadRemateLotAlerts(remateId) {
18299    const id = String(remateId || '').trim();
18300    if (!id || !this.state.user) {
18301      this.setState({ remateLotAlerts: {}, remateLotAlertsLoading: false });
18302      return;
18303    }
18304    this.setState({ remateLotAlertsLoading: true });
18305    try {
18306      const response = await this.apiFetch('/api/notifications/remate-lot-alerts?remateId=' + encodeURIComponent(id), { cache: 'no-store' });
18307      if (this.remateRouteId(this.state.currentRemate) !== id) return;
18308      const alerts = Array.isArray(response?.alerts) ?response.alerts : [];
18309      const byKey = alerts.reduce((result, alert) => {
18310        result[this.remateLotAlertKey(alert.remateId, alert.lotCert)] = alert;
18311        return result;
18312      }, {});
18313      this.setState({ remateLotAlerts: byKey, remateLotAlertsLoading: false });
18314    } catch (error) {
18315      if (this.remateRouteId(this.state.currentRemate) === id) this.setState({ remateLotAlertsLoading: false });
18316      if (Number(error?.status) !== 401) console.warn('remate lot alerts', error);
18317    }
18318  }
18319  async toggleRemateLotAlert(remate, lot, event) {
18320    event?.preventDefault?.();
18321    event?.stopPropagation?.();
18322    if (!this.state.user) {
18323      this.showToast('Inicia sesion para recibir el aviso del lote.');
18324      return;
18325    }
18326    const remateId = this.remateRouteId(remate);
18327    const lotCert = this.remateLotRouteId(lot);
18328    const key = this.remateLotAlertKey(remateId, lotCert);
18329    if (!remateId || !lotCert || this.state.remateLotAlertBusy === key) return;
18330    const current = this.state.remateLotAlerts?.[key];
18331    const enabled = current?.enabled === true && current?.status !== 'notified';
18332    this.setState({ remateLotAlertBusy: key });
18333    try {
18334      if (enabled) {
18335        await this.apiFetch('/api/notifications/remate-lot-alerts', {
18336          method: 'DELETE',
18337          body: JSON.stringify({ remateId, lotCert }),
18338        });
18339        const next = { ...this.state.remateLotAlerts };
18340        delete next[key];
18341        this.setState({ remateLotAlerts: next, remateLotAlertBusy: '' });
18342        this.showToast('Aviso cancelado.');
18343        return;
18344      }
18345      const response = await this.apiFetch('/api/notifications/remate-lot-alerts', {
18346        method: 'POST',
18347        body: JSON.stringify({ remateId, lotCert }),
18348      });
18349      this.setState({
18350        remateLotAlerts: { ...this.state.remateLotAlerts, [key]: { ...(response?.alert || {}), remateId, lotCert, enabled: true, status: 'active' } },
18351        remateLotAlertBusy: '',
18352      });
18353      this.showToast('Aviso activado. Te contactaremos cuando el lote esté cerca.');
18354    } catch (error) {
18355      this.setState({ remateLotAlertBusy: '' });
18356      this.showToast(error?.message || 'No se pudo guardar el aviso.');
18357    }
18358  }
18359  rosganSalesOrderValue(lot, index = 0) {
18360    const value = Number(lot?.saleOrder ?? lot?.lotNumber ?? lot?.order ?? lot?.orden_venta ?? lot?.orden_final ?? lot?.displayOrder);
18361    return Number.isFinite(value) && value > 0 ?value : index + 1;
18362  }
18363  normalizeRosganSalesOrder(lots) {
18364    return (Array.isArray(lots) ?lots : []).map((lot, index) => ({
18365      ...lot,
18366      order: index + 1,
18367      saleOrder: index + 1,
18368      lotNumber: lot.lotNumber ?? lot.orden_final ?? lot.order ?? index + 1,
18369      orden_final: lot.lotNumber ?? lot.orden_final ?? lot.order ?? index + 1,
18370      orden_venta: index + 1,
18371    }));
18372  }
18373  compareRosganSalesOrder(a,b){
18374    return this.rosganSalesOrderValue(a)-this.rosganSalesOrderValue(b) || Number(a.lotNumber ?? a.orden_final ?? 0)-Number(b.lotNumber ?? b.orden_final ?? 0);
18375  }
18376  sortedRosganSalesOrder(lots) {
18377    return [...(Array.isArray(lots) ?lots : [])].sort((a, b) => this.compareRosganSalesOrder(a,b));
18378  }
18379  rosganSalesOrderMedia(lot = {}, videosOverride = null) {
18380    const urlOf = (video) => typeof video === 'string' ?video : (video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl || '');
18381    const kindOf = (video, fallback = 'crudo') => {
18382      const type = String(video?.videoType || video?.type || video?.kind || video?.category || video?.status || video?.fileName || video?.name || '').toLowerCase();
18383      return video?.isFinal === true || /final|editado|aprobado/.test(type) ?'final' : fallback;
18384    };
18385    const finalUrl = urlOf(lot.finalVideo);
18386    if (finalUrl) return { url: finalUrl, kind: 'final', label: 'FINAL' };
18387    const media = (Array.isArray(videosOverride) ?videosOverride : [
18388      ...(Array.isArray(lot.videos) ?lot.videos : []),
18389      ...(Array.isArray(lot.videoAssets) ?lot.videoAssets : []),
18390      ...(Array.isArray(lot.rawVideos) ?lot.rawVideos : []),
18391    ]).filter((video) => urlOf(video));
18392    const final = media.find((video) => kindOf(video) === 'final');
18393    if (final) return { url: urlOf(final), kind: 'final', label: 'FINAL' };
18394    if (media[0]) return { url: urlOf(media[0]), kind: 'raw', label: 'CRUDO' };
18395    const fallbackUrl = lot.videoUrl || lot.video_uri || '';
18396    return fallbackUrl ?{ url: fallbackUrl, kind: 'final', label: 'FINAL' } : { url: '', kind: '', label: '' };
18397  }
18398  async hydrateRosganSalesOrderMedia(remate, lots) {
18399    const remateId = this.remateRouteId(remate);
18400    if (!remateId || !Array.isArray(lots) || !lots.length) return;
18401    const queue = [...lots];
18402    const resolved = new Map();
18403    const worker = async () => {
18404      while (queue.length) {
18405        const lot = queue.shift();
18406        const cert = this.remateLotRouteId(lot);
18407        if (!cert) continue;
18408        let media = this.rosganSalesOrderMedia(lot);
18409        try {
18410          const response = await this.apiFetch(this.rosganLotVideosEndpoint(remateId, cert), { cache: 'no-store' });
18411          media = this.rosganSalesOrderMedia(lot, this.normalizeRosganLotVideos(response));
18412        } catch {}
18413        resolved.set(cert, media);
18414      }
18415    };
18416    await Promise.all(Array.from({ length: Math.min(4, lots.length) }, worker));
18417    if (this.state.route !== 'rosganSalesOrder' || this.remateRouteId(this.state.currentRemate) !== remateId) return;
18418    this.setState((state) => ({
18419      rosganSalesOrderLots: (state.rosganSalesOrderLots || []).map((lot) => {
18420        const media = resolved.get(this.remateLotRouteId(lot));
18421        return media ?{ ...lot, salesVideoUrl: media.url, salesVideoKind: media.kind, salesVideoLabel: media.label } : lot;
18422      }),
18423    }));
18424  }
18425  async hydrateRosganSalesOrderMetadata(remate, lots) {
18426    const remateId = this.remateRouteId(remate);
18427    if (!remateId || !Array.isArray(lots) || !lots.length) return;
18428    let metadata = {};
18429    try {
18430      const response = await fetch('/assets/remates/' + encodeURIComponent(remateId) + '/lot-metadata.json', { cache: 'no-store' });
18431      if (response.ok) metadata = await response.json();
18432    } catch {}
18433    const consignatariaIds = [...new Set(lots.map((lot) => String(lot?.consignatariaId || '').trim()).filter(Boolean))];
18434    const establishments = new Map();
18435    await Promise.all(consignatariaIds.map(async (consignatariaId) => {
18436      try {
18437        const response = await this.apiFetch('/api/rosgan-establishments?consignatariaId=' + encodeURIComponent(consignatariaId));
18438        this.rosganArray(response).forEach((item) => {
18439          const id = String(item?.id || item?.establishmentId || item?.establecimientoId || '').trim();
18440          if (id) establishments.set(id, item);
18441        });
18442      } catch {}
18443    }));
18444    if (this.state.route !== 'rosganSalesOrder' || this.remateRouteId(this.state.currentRemate) !== remateId) return;
18445    const enrich = (lot, index) => {
18446      const cert = this.remateLotRouteId(lot);
18447      const stored = metadata?.[cert] && typeof metadata[cert] === 'object' ?metadata[cert] : {};
18448      const establishmentId = String(lot?.establishmentId || lot?.establecimientoId || '').trim();
18449      const establishment = establishments.get(establishmentId) || {};
18450      return this.normalizeRosganActiveLot({
18451        ...lot,
18452        locality: lot?.locality || lot?.nombre_localidad || lot?.localidad || lot?.establishmentLocality || lot?.establecimientoLocalidad || establishment?.locality || establishment?.localidad || stored.locality || '',
18453        province: lot?.province || lot?.nombre_provincia || lot?.provincia || lot?.establishmentProvince || lot?.establecimientoProvincia || establishment?.province || establishment?.provincia || stored.province || '',
18454        paymentTerm: lot?.paymentTerm || lot?.plazo || lot?.commercial?.paymentTerm || lot?.sale?.paymentTerm || stored.paymentTerm || '',
18455      }, index, remate);
18456    };
18457    this.setState((state) => ({
18458      remateLots: (state.remateLots || []).map(enrich),
18459      rosganSalesOrderLots: (state.rosganSalesOrderLots || []).map(enrich),
18460    }));
18461  }
18462  setRosganSalesOrderRating(lot, value) {
18463    const cert = this.remateLotRouteId(lot);
18464    const rating = Number(value);
18465    if (!cert || !Number.isInteger(rating) || rating < 1 || rating > 6) return;
18466    const current = this.loadRosganOpsState(this.state.currentRemate);
18467    this.saveRosganOpsState({ ...current, ratings: { ...current.ratings, [cert]: rating } }, this.state.currentRemate);
18468  }
18469  toggleRosganSalesOrderVideo(lot) {
18470    const cert = this.remateLotRouteId(lot);
18471    if (!cert) return;
18472    this.setState({ rosganSalesOrderVideoCert: this.state.rosganSalesOrderVideoCert === cert ?'' : cert });
18473  }
18474  rosganSalesOrderVideoStage(event) {
18475    return event?.currentTarget?.closest?.('.sales-order-video-stage') || event?.target?.closest?.('.sales-order-video-stage') || null;
18476  }
18477  async playRosganSalesOrderVideo(lot, event) {
18478    const cert = this.remateLotRouteId(lot);
18479    const stage = this.rosganSalesOrderVideoStage(event);
18480    const video = stage?.querySelector?.('video') || (cert ?document.getElementById('sales-order-video-' + cert) : null);
18481    if (!video) { this.showToast('No se encontró el reproductor del lote.'); return; }
18482    try {
18483      video.muted = false;
18484      await video.play();
18485      stage?.classList?.add('is-playing');
18486      stage?.classList?.remove('is-error');
18487    } catch {
18488      stage?.classList?.add('is-error');
18489      this.showToast('El navegador no pudo iniciar el video. Usá “Abrir video directamente”.');
18490    }
18491  }
18492  markRosganSalesOrderVideoPlaying(event) {
18493    const stage = this.rosganSalesOrderVideoStage(event);
18494    stage?.classList?.add('is-playing');
18495    stage?.classList?.remove('is-error');
18496  }
18497  markRosganSalesOrderVideoPaused(event) {
18498    this.rosganSalesOrderVideoStage(event)?.classList?.remove('is-playing');
18499  }
18500  markRosganSalesOrderVideoError(event) {
18501    const stage = this.rosganSalesOrderVideoStage(event);
18502    stage?.classList?.remove('is-playing');
18503    stage?.classList?.add('is-error');
18504  }
18505  openRosganSalesOrder() {
18506    this.openRosganSalesOrderPage(this.state.currentRemate, null, true, true);
18507  }
18508  recalculateRosganSalesOrderSchedule(lots, value = this.state.rosganSalesOrderSchedule) {
18509    const schedule = this.normalizeRosganSalesOrderSchedule(value);
18510    if (schedule.startSeconds == null || !schedule.entries.length) return schedule;
18511    const byCert = new Map(schedule.entries.map((entry) => [entry.cert, entry]));
18512    let elapsed = 0;
18513    const entries = (Array.isArray(lots) ?lots : []).flatMap((lot) => {
18514      const previous = byCert.get(this.remateLotRouteId(lot));
18515      if (!previous) return [];
18516      const next = { ...previous, estimatedStartSeconds: schedule.startSeconds + elapsed };
18517      elapsed += previous.averageSeconds;
18518      return [next];
18519    });
18520    return this.normalizeRosganSalesOrderSchedule({ ...schedule, entries, endSeconds: schedule.startSeconds + elapsed });
18521  }
18522  rosganExcelCellValue(value) {
18523    if (value == null || value instanceof Date || typeof value !== 'object') return value;
18524    if (value.result != null) return this.rosganExcelCellValue(value.result);
18525    if (value.text != null) return value.text;
18526    if (Array.isArray(value.richText)) return value.richText.map((part) => part?.text || '').join('');
18527    return String(value);
18528  }
18529  async exportRosganPreorderExcel() {
18530    if (!this.canManageUsers() || this.state.rosganPreorderExporting || this.state.remateLotsLoading) return;
18531    const remate = this.state.currentRemate, remateId = this.remateRouteId(remate);
18532    if (!remateId) return;
18533    const company = this.state.rosganSalesOrderConsignatariaFilter || '';
18534    const ratings = { ...this.state.rosganSalesOrderRatings };
18535    const companyIds = new Set((this.state.rosganSalesOrderLots || []).filter(lot => (this.consDisplayName(lot.nombre_consignatario || lot.consignatariaNombre || remate.nombre_cuenta || remate.nombre_consignatario) || 'Sin consignataria') === company).map(lot => String(lot.consignatariaId || '')));
18536    this.setState({ rosganPreorderExporting: true, rosganSalesOrderError: '' });
18537    try {
18538      const [fullResponse, auditResponse] = await Promise.all([
18539        this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots/all', { cache: 'no-store' }),
18540        this.apiFetch('/api/auditor/remates/' + encodeURIComponent(remateId) + '/lotes', { cache: 'no-store' }),
18541      ]);
18542      const fullByCert = new Map(this.rosganArray(fullResponse).map(lot => [String(lot.lotCert || lot.cert || lot.id), lot]));
18543      const response = this.rosganArray(auditResponse).map(lot => {
18544        const full = fullByCert.get(String(lot.lotCert || lot.cert || lot.id));
18545        if (!full) throw new Error('No se pudo obtener el detalle completo del certificado ' + lot.lotCert + '. Reintentá la descarga.');
18546        return { ...lot, ...full, consignatariaName: full.consignatariaName || lot.consignatariaName,
18547          locality: lot.locality || full.locality, province: lot.province || full.province,
18548          paymentTerm: lot.paymentTerm || full.paymentTerm };
18549      });
18550      const lots = this.rosganArray(response).map(lot => ({ ...lot, consignatariaName: lot.consignatariaName || this.consName((this.state.consignatarias || []).find(item => String(item.id) === String(lot.consignatariaId)) || {}) || lot.consignatariaId }));
18551      const selected = lots.filter(lot => window.RosganPreorderExcel.isEligible(lot) && (!company || companyIds.has(String(lot.consignatariaId || ''))));
18552      if (!selected.length) throw new Error('No hay lotes para generar el preorden de esta selección.');
18553      const ExcelJS = await this.ensureCabanaExcelJs();
18554      const book = window.RosganPreorderExcel.build(ExcelJS, selected, { ratings, companyColor: value => this.consBrandColor(value) });
18555      const blob = new Blob([await book.xlsx.writeBuffer()], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
18556      this.downloadCabanaBlob(blob, window.RosganPreorderExcel.filename(remate.nombre_remate || remate.title || remate.name || 'Remate ' + remateId, company));
18557      this.showToast('Preorden de venta descargado.');
18558    } catch (error) { this.setState({ rosganSalesOrderError: error?.message || 'No se pudo generar el preorden. Reintentá la descarga.' }); }
18559    finally { this.setState({ rosganPreorderExporting: false }); }
18560  }
18561  async importRosganSalesOrderExcel(event) {
18562    const input = event?.target;
18563    const file = input?.files?.[0];
18564    if (input) input.value = '';
18565    if (!file || this.state.rosganSalesOrderImporting || this.state.rosganSalesOrderSaving) return;
18566    if (!/\.xlsx$/i.test(file.name || '')) {
18567      this.setState({ rosganSalesOrderError: 'El archivo debe estar en formato .xlsx.' });
18568      return;
18569    }
18570    if (Number(file.size) > 8 * 1024 * 1024) {
18571      this.setState({ rosganSalesOrderError: 'El Excel supera el máximo permitido de 8 MB.' });
18572      return;
18573    }
18574    this.setState({ rosganSalesOrderImporting: true, rosganSalesOrderError: '', rosganSalesOrderSaved: false, rosganOrderCommandMessage: '' });
18575    try {
18576      const ExcelJS = await this.ensureCabanaExcelJs();
18577      const workbook = new ExcelJS.Workbook();
18578      await workbook.xlsx.load(await file.arrayBuffer());
18579      const worksheet = workbook.worksheets.find((sheet) => Number(sheet.actualRowCount || sheet.rowCount) > 0);
18580      if (!worksheet) throw new Error('El Excel no contiene hojas con datos.');
18581      const columnCount = Math.max(4, Math.min(50, Number(worksheet.actualColumnCount || worksheet.columnCount) || 4));
18582      const rows = [];
18583      worksheet.eachRow({ includeEmpty: true }, (row) => {
18584        rows.push(Array.from({ length: columnCount }, (_, index) => this.rosganExcelCellValue(row.getCell(index + 1).value)));
18585      });
18586      const plan = window.RosganSalesOrderImport.parse(rows);
18587      const ordered = window.RosganSalesOrderImport.apply(plan, this.state.rosganSalesOrderLots || [], (lot) => this.remateLotRouteId(lot));
18588      const schedule = this.normalizeRosganSalesOrderSchedule({
18589        fileName: file.name,
18590        importedAt: new Date().toISOString(),
18591        startSeconds: plan.startSeconds,
18592        endSeconds: plan.endSeconds,
18593        entries: plan.entries,
18594      });
18595      this.setState({
18596        rosganSalesOrderLots: this.normalizeRosganSalesOrder(ordered),
18597        rosganSalesOrderSchedule: schedule,
18598        rosganSalesOrderImporting: false,
18599        rosganSalesOrderError: '',
18600        rosganSalesOrderSaved: false,
18601        rosganOrderCommandMessage: 'Excel cargado: revisá el orden y guardalo para confirmarlo.',
18602      });
18603    } catch (error) {
18604      this.setState({ rosganSalesOrderImporting: false, rosganSalesOrderError: error?.message || 'No se pudo interpretar el Excel.' });
18605    }
18606  }
18607  openRosganSalesOrderPage(item, lotsOverride = null, syncPath = true, forceFetch = false) {
18608    const remate = this.normalizeRosganActiveRemate(item || {});
18609    const remateId = this.remateRouteId(remate);
18610    if (!remateId) return;
18611    const useCurrent = this.remateRouteId(this.state.currentRemate) === remateId && Array.isArray(this.state.remateLots);
18612    const source = Array.isArray(lotsOverride) ?lotsOverride : (!forceFetch && useCurrent ?this.state.remateLots : null);
18613    const openWithLots = (lots) => {
18614      const normalized = (Array.isArray(lots) ?lots : [])
18615        .filter((lot) => !['anulado', 'cerrado'].includes(this.rosganLotWorkflowStatus(lot)))
18616        .map((lot, index) => lot.__rosganActiveLot?lot:this.normalizeRosganActiveLot(lot, index));
18617      const stored = this.loadRosganOpsState(remate);
18618      this.setState({
18619        route: 'rosganSalesOrder', rosganOrderCommandMessage:'', currentRemate: remate, remateLots: normalized, remateLotsLoading: false,
18620        rosganSalesOrderLots: this.sortedRosganSalesOrder(normalized), rosganSalesOrderSaving: false,
18621        rosganSalesOrderError: '', rosganSalesOrderSaved: false, rosganSalesOrderVideoCert: '', rdLightbox: null,
18622        rosganSalesOrderQuery: '', rosganSalesOrderStatusFilter: '', rosganSalesOrderCategoryFilter: '', rosganSalesOrderConsignatariaFilter: '', rosganSalesOrderRatingFilter: '', rosganSalesOrderRatingSnapshot: null,
18623        rosganOpsDrafts: stored.commercial, rosganOpsConfig: stored.tv, rosganSalesOrderRatings: stored.ratings, rosganSalesOrderSchedule: stored.salesOrder,
18624      }, () => {
18625        this.fetchRosganOpsRemoteState({ force: true, silent: true });
18626        this.fetchCatalogPublication(remate);
18627        this.hydrateRosganSalesOrderMetadata(remate, normalized);
18628        this.hydrateRosganSalesOrderMedia(remate, normalized);
18629      });
18630      window.scrollTo(0, 0);
18631    };
18632    if (syncPath) {
18633      try { window.history.pushState({}, '', '/remates/' + encodeURIComponent(remateId) + '/orden-ventas'); } catch {}
18634    }
18635    if (source) {
18636      openWithLots(source);
18637      return;
18638    }
18639    this.setState({
18640      route: 'rosganSalesOrder', currentRemate: remate, remateLotsLoading: true,
18641      rosganSalesOrderLots: [], rosganSalesOrderSaving: false, rosganSalesOrderImporting: false, rosganSalesOrderSchedule: null, rosganSalesOrderError: '', rosganSalesOrderSaved: false,
18642      rosganSalesOrderQuery: '', rosganSalesOrderStatusFilter: '', rosganSalesOrderCategoryFilter: '', rosganSalesOrderConsignatariaFilter: '', rosganSalesOrderRatingFilter: '', rosganSalesOrderRatingSnapshot: null,
18643    });
18644    Promise.all([
18645      this.apiFetch('/api/auditor/remates/' + encodeURIComponent(remateId) + '/lotes', { cache: 'no-store' }).catch(() => null),
18646      this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots', { cache: 'no-store' }).catch(() => null),
18647    ])
18648      .then(([auditorResponse, catalogResponse]) => {
18649        const audited = auditorResponse ?this.rosganArray(auditorResponse) : [];
18650        const catalog = catalogResponse ?this.rosganArray(catalogResponse) : [];
18651        if (!audited.length && !catalog.length) throw new Error('No se pudieron cargar los lotes.');
18652        const catalogByCert = new Map(catalog.map((lot) => [this.remateLotRouteId(lot), lot]));
18653        openWithLots(audited.length ?audited.map((lot) => ({ ...(catalogByCert.get(this.remateLotRouteId(lot)) || {}), ...lot })) : catalog);
18654      })
18655      .catch((error) => this.setState({ remateLotsLoading: false, rosganSalesOrderError: error?.message || 'No se pudieron cargar los lotes.' }));
18656  }
18657  async openRosganSalesOrderById(remateId) {
18658    const requestedPath = window.location.pathname;
18659    try {
18660      const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId));
18661      if (window.location.pathname !== requestedPath) return;
18662      const remate = response?.data ?? response;
18663      if (!remate || remate?.cabanasConfig) throw new Error('No es un remate Rosgan activo.');
18664      return this.openRosganSalesOrderPage(remate, null, false, true);
18665    } catch (error) {
18666      if (window.location.pathname !== requestedPath) return;
18667      this.showToast(error?.message || 'No se pudo abrir el orden de venta.');
18668      this.go('remates');
18669    }
18670  }
18671  closeRosganSalesOrder() {
18672    if (this.state.rosganSalesOrderSaving) return;
18673    const remate = this.state.currentRemate;
18674    this.setState({ route: 'remateDetail', rdSort: 'orden', rosganSalesOrderError: '', rosganSalesOrderSaved: false });
18675    this.syncRemateLocation(remate, null, { replace: true });
18676    window.scrollTo(0, 0);
18677  }
18678  moveRosganSalesOrder(index, offset) {
18679    const lots = [...(this.state.rosganSalesOrderLots || [])];
18680    const target = index + offset;
18681    if (index < 0 || target < 0 || index >= lots.length || target >= lots.length) return;
18682    const [moved] = lots.splice(index, 1);
18683    lots.splice(target, 0, moved);
18684    const normalized = this.normalizeRosganSalesOrder(lots);
18685    this.setState({ rosganSalesOrderLots: normalized, rosganSalesOrderSchedule: this.recalculateRosganSalesOrderSchedule(normalized), rosganSalesOrderError: '', rosganSalesOrderSaved: false });
18686  }
18687  setRosganSalesOrderPosition(index, event) {
18688    const lots = [...(this.state.rosganSalesOrderLots || [])];
18689    const requested=Number(event?.target?.value);
18690    if(!Number.isInteger(requested)||requested<1){if(event?.target)event.target.value=index+1;return;}
18691    const target = Math.max(0, Math.min(lots.length - 1, requested - 1));
18692    if (index < 0 || index >= lots.length || target === index) return;
18693    const [moved] = lots.splice(index, 1);
18694    lots.splice(target, 0, moved);
18695    const normalized = this.normalizeRosganSalesOrder(lots);
18696    this.setState({ rosganSalesOrderLots: normalized, rosganSalesOrderSchedule: this.recalculateRosganSalesOrderSchedule(normalized), rosganSalesOrderError: '', rosganSalesOrderSaved: false });
18697  }
18698  applyRosganOrderCommand(){
18699    if(this.state.rosganSalesOrderSaving||this.state.remateLotsLoading)return;
18700    try{
18701      const result=window.RosganOrderCommand.move(this.state.rosganSalesOrderLots||[],document.getElementById('rg-order-command')?.value);
18702      const normalized=this.normalizeRosganSalesOrder(result.lots);
18703      this.setState({rosganSalesOrderLots:normalized,rosganSalesOrderSchedule:this.recalculateRosganSalesOrderSchedule(normalized),rosganSalesOrderError:'',rosganSalesOrderSaved:false,rosganOrderCommandMessage:result.message});
18704    }catch(error){this.setState({rosganSalesOrderError:error.message,rosganOrderCommandMessage:''});}
18705  }
18706  async saveRosganSalesOrder() {
18707    if (!this.canManageUsers() || this.state.rosganSalesOrderSaving) return;
18708    const remateId = this.remateRouteId(this.state.currentRemate);
18709    const ordered = this.normalizeRosganSalesOrder(this.state.rosganSalesOrderLots || []);
18710    const orders = ordered.map((lot, index) => ({ lotCert: this.remateLotRouteId(lot), displayOrder: index + 1 })).filter((item) => item.lotCert);
18711    if (!remateId || !orders.length) {
18712      this.setState({ rosganSalesOrderError: 'No hay lotes para ordenar.' });
18713      return;
18714    }
18715    this.setState({ rosganSalesOrderSaving: true, rosganSalesOrderError: '', rosganSalesOrderSaved: false });
18716    try {
18717      const publication = await this.apiFetch('/api/catalog-publication/' + encodeURIComponent(remateId) + '/sales-order', {
18718        method: 'PUT', body: JSON.stringify({ orders, schedule: this.recalculateRosganSalesOrderSchedule(ordered) }),
18719      });
18720      this.setState({ catalogPublication: { key: this.catalogPublicationKey(), published: publication.published === true, salesOrderDefined: true, loaded: true, busy: false } });
18721      const currentOps = this.loadRosganOpsState(this.state.currentRemate);
18722      this.saveRosganOpsState({ ...currentOps, salesOrder: this.recalculateRosganSalesOrderSchedule(ordered) }, this.state.currentRemate);
18723      this.setState({ remateLots: ordered, rosganSalesOrderLots: ordered, rosganOrderCommandMessage:'', rosganSalesOrderSaving: false, rosganSalesOrderSaved: true });
18724    } catch (error) {
18725      this.setState({ rosganSalesOrderSaving: false, rosganSalesOrderError: error?.message || 'No se pudo guardar el orden de venta.' });
18726    }
18727  }
18728  async fetchRemateLots(item, lotId = '') {
18729    const id = item.id_remate || item.sourceId || item.id || '';
18730    const requestedPath = window.location.pathname;
18731    this.setState({ remateLotsLoading: true, remateLotsReal: false });
18732    try {
18733    const data = await this.unwrap('/api/remateshistorico?remateId=' + encodeURIComponent(id) + '&lots=1');
18734      if (data === null) throw 0;
18735      const lots = Array.isArray(data?.items) ?data.items : (Array.isArray(data) ?data : []);
18736      const currentRemate = { ...item, ...(data?.remate || {}) };
18737      if (window.location.pathname !== requestedPath) return;
18738      const targetLot = lotId ?lots.find((candidate) => this.remateLotRouteId(candidate) === String(lotId)) || null : null;
18739      this.setState({ remateLots: lots, currentRemate, rdLightbox: targetLot, remateLotsReal: true, remateLotsLoading: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true });
18740    } catch { if (window.location.pathname !== requestedPath) return; this.setState({ remateLots: [], remateLotsReal: false, remateLotsLoading: false }); }
18741  }
18742  remateLotNeighbors(lot, lots = this.state.remateLots) {
18743    const ordered = [...this.publicRemateLots(lots)].sort((a, b) => this.compareRosganSalesOrder(a, b));
18744    const cert = lot && this.remateLotRouteId(lot);
18745    const index = cert ? ordered.findIndex(item => this.remateLotRouteId(item) === cert) : -1;
18746    return { previous: index > 0 ? ordered[index - 1] : null, next: index >= 0 ? ordered[index + 1] || null : null };
18747  }
18748  openRemateLot(item, options = {}) {
18749    if (!item || !this.state.currentRemate) return;
18750    if (this.state.currentRemate.__rosganActive && !this.isRosganLotPublished(item)) return;
18751    if (options.syncPath !== false) this.syncRemateLocation(this.state.currentRemate, item);
18752    this.fetchRosganRepresentative(item);
18753    this.setState({ rdLightbox: item, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true });
18754    if (this.state.currentRemate.__rosganActive) this.fetchRosganLotVideos(this.remateRouteId(this.state.currentRemate), this.remateLotRouteId(item));
18755  }
18756  closeRemateLot() {
18757    if (this.state.currentRemate) this.syncRemateLocation(this.state.currentRemate, null, { replace: true });
18758    this.setState({ rdLightbox: null, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true });
18759  }
18760  rosganLotVideoKey(remateId, lotCert) { return String(remateId) + ':' + String(lotCert); }
18761  async prepareFieldLot(context = null) {
18762    const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate);
18763    const cert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox);
18764    const lot = context?.lotCert ?context : this.state.rdLightbox;
18765    if (!remateId || !cert || !this.canEditRosganLot(lot) || this._preparingFieldLot) return;
18766    this._preparingFieldLot = true;
18767    this.showToast('Preparando cámara y lote para usar sin señal…');
18768    try { await window.RosganField.prepare(remateId, cert); }
18769    catch (error) { this.showToast(error.message || 'No se pudo preparar el modo campo.'); }
18770    finally { this._preparingFieldLot = false; }
18771  }
18772  rosganLotVideosEndpoint(remateId, lotCert, suffix = '') {
18773    return '/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots/' + encodeURIComponent(lotCert) + '/videos' + suffix;
18774  }
18775  updateRosganLotVideoState(key, patch) {
18776    this.setState((state) => ({ rosganLotVideoState: { ...state.rosganLotVideoState, [key]: { loading: false, videos: [], uploads: [], error: '', ...(state.rosganLotVideoState?.[key] || {}), ...patch } } }));
18777  }
18778  normalizeRosganLotVideos(data) {
18779    const root = data?.data ?? data;
18780    if (Array.isArray(root)) return root;
18781    for (const key of ['items', 'videos', 'results']) if (Array.isArray(root?.[key])) return root[key];
18782    return root?.id || root?.videoId ?[root] : [];
18783  }
18784  formatFileSize(bytes) {
18785    const size = Number(bytes) || 0;
18786    if (!size) return 'Tamaño no informado';
18787    if (size < 1024 * 1024) return Math.max(1, Math.round(size / 1024)) + ' KB';
18788    return (size / (1024 * 1024)).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + ' MB';
18789  }
18790  async fetchRosganLotVideos(remateId, lotCert, force = false) {
18791    if (!remateId || !lotCert) return;
18792    const key = this.rosganLotVideoKey(remateId, lotCert);
18793    const current = this.state.rosganLotVideoState?.[key];
18794    if (current?.loading || (!force && Array.isArray(current?.videos))) return;
18795    this.updateRosganLotVideoState(key, { loading: true, error: '' });
18796    try {
18797      const response = await this.apiFetch(this.rosganLotVideosEndpoint(remateId, lotCert));
18798      this.updateRosganLotVideoState(key, { loading: false, videos: this.normalizeRosganLotVideos(response), error: '' });
18799      this.restoreRosganRawVideoSession(remateId, lotCert);
18800    } catch (error) {
18801      this.updateRosganLotVideoState(key, { loading: false, error: error?.message || 'No se pudieron cargar los videos.' });
18802    }
18803  }
18804  updateRosganVideoUpload(key, uploadId, patch) {
18805    this.setState((state) => {
18806      const entry = state.rosganLotVideoState?.[key] || { loading: false, videos: [], uploads: [], error: '' };
18807      const uploads = (entry.uploads || []).map((upload) => upload.id === uploadId ?{ ...upload, ...patch } : upload);
18808      return { rosganLotVideoState: { ...state.rosganLotVideoState, [key]: { ...entry, uploads } } };
18809    });
18810  }
18811  async uploadRosganLotVideos(fileList, context = null) {
18812    const explicitContext = !!(context?.remateId && context?.lotCert);
18813    const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate);
18814    const lotCert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox);
18815    const lot = explicitContext ?context : this.state.rdLightbox;
18816    if ((!explicitContext && !this.state.currentRemate?.__rosganActive) || !remateId || !lotCert || !this.canEditRosganLot(lot)) return;
18817    const files = Array.from(fileList || []), key = this.rosganLotVideoKey(remateId, lotCert);
18818    this._rawVideoBatches ||= new Set();
18819    if (this._rawVideoBatches.has(key)) return;
18820    const validation = window.RosganRawVideoBatch.validate(files);
18821    if (validation) { this.updateRosganLotVideoState(key, { error: validation }); return; }
18822    try {
18823      await window.RosganVideoQueue.enqueue(remateId,lotCert,files,'raw');
18824      this.updateRosganLotVideoState(key,{error:''});
18825      this.showToast('Videos guardados en la cola. Podés seguir el progreso en el panel de cargas.');
18826    }catch(error){this.updateRosganLotVideoState(key,{error:error.message||'No se pudo iniciar la carga.'});}
18827  }
18828
18829  createRosganRawVideoSession(key, remateId, lotCert, files, pendingMetadata = []) {
18830    const uid = this.state.user?.uid;
18831    const session = window.RosganRawVideoBatch.createSession((...args) => {
18832      if (this.state.user?.uid !== uid) throw Error('La sesión cambió. Volvé a ingresar con la cuenta que inició la subida.');
18833      return this.apiFetch(...args);
18834    }, remateId, lotCert, files, {
18835      pendingMetadata,
18836      onChange: snapshot => { if (this.state.user?.uid === uid) this.syncRosganRawVideoSession(key, snapshot); },
18837    });
18838    this._rawVideoSessions ||= new Map();
18839    this._rawVideoSessions.set(key, { session, uid });
18840    return session;
18841  }
18842  restoreRosganRawVideoSession(remateId, lotCert) {
18843    const key = this.rosganLotVideoKey(remateId, lotCert), uid = this.state.user?.uid;
18844    if (!uid || this._rawVideoSessions?.get(key)?.uid === uid) return;
18845    try {
18846      const storageKey = 'rosgan-raw-metadata:' + uid + ':' + key;
18847      const pending = JSON.parse(window.sessionStorage.getItem(storageKey) || '[]');
18848      const current = this.state.rosganLotVideoState?.[key]?.videos || [];
18849      const missing = pending.filter(m => !current.some(v => v.fileKey === m.fileKey));
18850      if (!missing.length) { window.sessionStorage.removeItem(storageKey); return; }
18851      const session = this.createRosganRawVideoSession(key, remateId, lotCert, [], missing);
18852      this.syncRosganRawVideoSession(key, session.snapshot());
18853    } catch {}
18854  }
18855  syncRosganRawVideoSession(key, snapshot) {
18856    try {
18857      const storageKey = 'rosgan-raw-metadata:' + this.state.user?.uid + ':' + key;
18858      if (snapshot.pendingMetadata?.length) window.sessionStorage.setItem(storageKey, JSON.stringify(snapshot.pendingMetadata));
18859      else window.sessionStorage.removeItem(storageKey);
18860    } catch {}
18861    this.setState(state => {
18862      const current = state.rosganLotVideoState?.[key] || { videos: [] };
18863      const merged = new Map([...(current.videos || []), ...snapshot.jobs.map(j => j.video).filter(Boolean)].map(v => [v.id, v]));
18864      return { rosganLotVideoState: { ...state.rosganLotVideoState, [key]: { ...current, uploading: snapshot.running, uploads: snapshot.jobs, videos: [...merged.values()].sort((a,b) => a.order-b.order), error: snapshot.jobs.find(j => j.error)?.error || '' } } };
18865    });
18866  }
18867  async runRosganRawVideoSession(key, remateId, lotCert, session, retryId = null) {
18868    this._rawVideoBatches ||= new Set();
18869    if (this._rawVideoBatches.has(key)) return;
18870    this._rawVideoBatches.add(key);
18871    try {
18872      const result = retryId == null ? await session.start() : await session.retry(retryId);
18873      if (this._rawVideoSessions?.get(key)?.uid !== this.state.user?.uid) return;
18874      if (result.jobs.every(j => j.status === 'Listo')) this.showToast(result.jobs.length + ' video(s) cargados correctamente.');
18875      else {
18876        await this.fetchRosganLotVideos(remateId, lotCert, true);
18877        this.syncRosganRawVideoSession(key, session.snapshot());
18878      }
18879    } finally { this._rawVideoBatches.delete(key); }
18880  }
18881  async retryRosganVideoUpload(uploadId, context = null) {
18882    const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate);
18883    const lotCert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox);
18884    const lot = context?.remateId && context?.lotCert ? context : this.state.rdLightbox;
18885    if (!remateId || !lotCert || !this.canEditRosganLot(lot)) return;
18886    const key = this.rosganLotVideoKey(remateId, lotCert), entry = this._rawVideoSessions?.get(key);
18887    if (entry?.uid !== this.state.user?.uid || !entry?.session) return;
18888    await this.runRosganRawVideoSession(key, remateId, lotCert, entry.session, uploadId);
18889  }
18890
18891  async deleteRosganLotVideo(video, context = null) {
18892    const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate);
18893    const lotCert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox);
18894    const videoId = video?.id || video?.videoId || video?.videoID;
18895    const lot = context?.remateId && context?.lotCert ?context : this.state.rdLightbox;
18896    if (!remateId || !lotCert || !videoId || !this.canEditRosganLot(lot) || String(video?.videoType || video?.type || '').toLowerCase() !== 'raw' || this.state.rosganLotVideoDeleting || !window.confirm('¿Eliminar este video del lote?')) return;
18897    const deletingKey = this.rosganLotVideoKey(remateId, lotCert) + ':' + videoId;
18898    this.setState({ rosganLotVideoDeleting: deletingKey });
18899    try {
18900      await this.apiFetch(this.rosganLotVideosEndpoint(remateId, lotCert, '/' + encodeURIComponent(videoId)), { method: 'DELETE' });
18901      this.setState({ rosganLotVideoDeleting: '' });
18902      await this.fetchRosganLotVideos(remateId, lotCert, true);
18903      this.showToast('Video eliminado.');
18904    } catch (error) {
18905      this.setState({ rosganLotVideoDeleting: '' });
18906      this.updateRosganLotVideoState(this.rosganLotVideoKey(remateId, lotCert), { error: error?.message || 'No se pudo eliminar el video.' });
18907    }
18908  }
18909  consName(c) { return (c && (c.name || c.nombre || c.slug)) || (typeof c === 'string' ?c : 'Canal'); }
18910  humanizeSlug(value) {
18911    return String(value || '').split(/[-_]+/).filter(Boolean).map((part) => {
18912      const lower = part.toLowerCase();
18913      if (lower === 'y') return 'y';
18914      if (lower === 's' || lower === 'a') return lower.toUpperCase() + '.';
18915      return lower.charAt(0).toUpperCase() + lower.slice(1);
18916    }).join(' ').replace(/\bS\. A\./g, 'S.A.');
18917  }
18918  // Imágenes del hero del canal ? placeholders de hacienda (reemplazables luego)
18919  channelHeroImages = ['assets/angus-field.png', 'assets/cabanas.png', 'assets/feria.png', 'assets/access-cabanas.png', 'assets/access-remates.png'];
18920  // Imágenes del hero de la home ? placeholders de hacienda (reemplazables luego)
18921  homeHeroImages = ['assets/angus-field.png', 'assets/feria.png', 'assets/cabanas.png', 'assets/access-remates.png', 'assets/access-cabanas.png'];
18922  rgba(color, a) {
18923    const value = String(color || '').trim();
18924    const hex = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
18925    if (hex) {
18926      const raw = hex[1].length === 3 ?hex[1].split('').map((char) => char + char).join('') : hex[1];
18927      return 'rgba(' + parseInt(raw.slice(0, 2), 16) + ',' + parseInt(raw.slice(2, 4), 16) + ',' + parseInt(raw.slice(4, 6), 16) + ',' + a + ')';
18928    }
18929    const rgb = value.match(/(\d+),\s*(\d+),\s*(\d+)/);
18930    return rgb ?'rgba(' + rgb[1] + ',' + rgb[2] + ',' + rgb[3] + ',' + a + ')' : value;
18931  }
18932  startChannelHeroAuto() {
18933    if (this._chHeroTimer || !(this.props.animaciones || true)) return;
18934    this._chHeroTimer = setInterval(() => {
18935      if (this.state.route !== 'channel') return;
18936      const n = (this.channelHeroImages || []).length;
18937      if (n > 1) this.setState((s) => ({ chHeroSlide: (s.chHeroSlide + 1) % n }));
18938    }, 5000);
18939  }
18940  openChannel(c) {
18941    const name = this.consName(c);
18942    const logo = this.consLogo(name) || (c && (c.logoUrl || c.logo || c.imageUrl));
18943    if (logo) this.extractLogoColor(name, logo);
18944    this.setState({ route: 'channel', selectedCons: c, consRemates: null, channelTab: 'todos', channelYear: null, chHeroSlide: 0 });
18945    window.scrollTo(0, 0);
18946    this.startChannelHeroAuto();
18947    this.fetchConsRemates(name);
18948  }
18949  async fetchConsRemates(name) {
18950    const key = this.consKey(name);
18951    this.setState({ consRematesLoading: true });
18952    const match = (r) => { const k = this.consKey(r.nombre_cuenta || r.nombre_consignatario); return k && (k === key || k.includes(key) || key.includes(k)); };
18953    const pull = (d) => Array.isArray(d) ?d : (d?.items || d?.remates || []);
18954    const base = '/api/remateshistorico';
18955    try {
18956      const yearNow = new Date().getFullYear();
18957      // 1er fetch: trae el año actual + la lista de años disponibles
18958    const first = await this.unwrap(base + '?scope=all&year=' + yearNow + '&page=1&pageSize=120');
18959      const years = [...new Set((first?.years || []).map(Number).filter(Boolean))].sort((a, b) => b - a);
18960      const yearList = (years.length ?years : [yearNow, yearNow - 1, yearNow - 2]).slice(0, 6);
18961      // resto de años en paralelo ? videoteca completa
18962    const rest = await Promise.all(yearList.filter((y) => y !== yearNow).map((y) => this.unwrap(base + '?scope=all&year=' + y + '&page=1&pageSize=120')));
18963      const all = [...pull(first), ...rest.flatMap(pull), ...(this.state.monthRemates || [])];
18964      const seen = new Set();
18965      const list = all.filter(match).filter((r) => { const id = r.id_remate || r.sourceId || r.id || JSON.stringify(r).slice(0, 40); if (seen.has(id)) return false; seen.add(id); return true; });
18966      this.setState({ consRemates: list, consYears: yearList, consRematesLoading: false });
18967    } catch { this.setState({ consRemates: [], consRematesLoading: false }); }
18968  }
18969  captureVideoThumbnail(url) {
18970    return new Promise((resolve, reject) => {
18971      const video = document.createElement('video');
18972      let settled = false;
18973      let timeoutId = null;
18974      video.crossOrigin = 'anonymous';
18975      video.muted = true;
18976      video.playsInline = true;
18977      video.preload = 'auto';
18978      const cleanup = () => {
18979        if (timeoutId) clearTimeout(timeoutId);
18980        video.pause();
18981        video.removeAttribute('src');
18982        video.load();
18983      };
18984      const fail = (message = 'No se pudo generar la miniatura.') => {
18985        if (settled) return;
18986        settled = true;
18987        cleanup();
18988        reject(new Error(message));
18989      };
18990      const drawFrame = () => {
18991        if (settled || video.readyState < 2) return;
18992        try {
18993          const width = video.videoWidth || 640;
18994          const height = video.videoHeight || 360;
18995          const canvas = document.createElement('canvas');
18996          canvas.width = 320;
18997          canvas.height = Math.max(1, Math.round((height / width) * canvas.width));
18998          canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
18999          const thumbnail = canvas.toDataURL('image/jpeg', 0.78);
19000          settled = true;
19001          cleanup();
19002          resolve(thumbnail);
19003        } catch {
19004          fail();
19005        }
19006      };
19007      video.addEventListener('loadedmetadata', () => {
19008        const duration = Number.isFinite(video.duration) && video.duration > 0 ?video.duration : 3;
19009        const target = Math.min(Math.max(duration * 0.18, 0.4), Math.max(duration - 0.1, 0));
19010        if (Math.abs(video.currentTime - target) < .05) drawFrame();
19011        else {
19012          try { video.currentTime = target; }
19013          catch { drawFrame(); }
19014        }
19015      }, { once: true });
19016      video.addEventListener('seeked', drawFrame, { once: true });
19017      video.addEventListener('loadeddata', () => {
19018        if (!video.seeking && video.currentTime > 0) drawFrame();
19019      });
19020      video.addEventListener('error', fail, { once: true });
19021      timeoutId = setTimeout(() => fail('La miniatura tardó demasiado en responder.'), 10000);
19022      video.src = url;
19023      video.load();
19024    });
19025  }
19026  hydrateVideoThumbnails(videos) {
19027    this._videoThumbLoading = this._videoThumbLoading || new Set();
19028    (videos || []).forEach((video) => {
19029      const url = video?.url || '';
19030      if (!url || video.thumb || this.state.videoThumbByUrl[url] || this.state.videoThumbFailedByUrl[url] || this._videoThumbLoading.has(url)) return;
19031      this._videoThumbLoading.add(url);
19032      this.captureVideoThumbnail(url)
19033        .then((thumbnail) => {
19034          this.setState((state) => ({
19035            videoThumbByUrl: { ...(state.videoThumbByUrl || {}), [url]: thumbnail },
19036            videoThumbFailedByUrl: { ...(state.videoThumbFailedByUrl || {}), [url]: false },
19037          }));
19038        })
19039        .catch((error) => {
19040          console.warn('[Roslot video thumbnail failed]', { url, message: error?.message });
19041          this.setState((state) => ({ videoThumbFailedByUrl: { ...(state.videoThumbFailedByUrl || {}), [url]: true } }));
19042        })
19043        .finally(() => this._videoThumbLoading.delete(url));
19044    });
19045  }
19046  lotViewSessionId() {
19047    const key = 'roslot.viewSessionId';
19048    try {
19049      const current = localStorage.getItem(key);
19050      if (current) return current;
19051      const next = window.crypto?.randomUUID?.() || ('view-' + Date.now() + '-' + Math.random().toString(16).slice(2));
19052      localStorage.setItem(key, next);
19053      return next;
19054    } catch {
19055      return 'view-' + Date.now() + '-' + Math.random().toString(16).slice(2);
19056    }
19057  }
19058  lotViewStorageKey(cert) {
19059    const bucket = Math.floor(Date.now() / (6 * 60 * 60 * 1000));
19060    return 'roslot.lotView.' + cert + '.' + bucket;
19061  }
19062  async loadLotViewCounter(cert, force = false) {
19063    cert = String(cert || '').trim();
19064    if (!cert || this.state.lotViewLoading[cert]) return;
19065    if (!force && Object.prototype.hasOwnProperty.call(this.state.lotViewCounts, cert)) return;
19066    this.setState({ lotViewLoading: { ...this.state.lotViewLoading, [cert]: true } });
19067    try {
19068      const data = await this.apiFetch('/api/lots/' + encodeURIComponent(cert) + '/views');
19069      if (Number.isFinite(Number(data?.views))) {
19070        this.setState({
19071          lotViewCounts: { ...this.state.lotViewCounts, [cert]: Number(data.views) },
19072          lotViewLoading: { ...this.state.lotViewLoading, [cert]: false },
19073        });
19074        return;
19075      }
19076    } catch (error) {
19077      if (![404, 405].includes(Number(error?.status))) console.warn('[Roslot lot view counter failed]', { cert, message: error?.message });
19078    }
19079    this.setState({ lotViewLoading: { ...this.state.lotViewLoading, [cert]: false } });
19080  }
19081  async recordLotView(cert) {
19082    cert = String(cert || '').trim();
19083    if (!cert) return;
19084    const storageKey = this.lotViewStorageKey(cert);
19085    try {
19086      if (localStorage.getItem(storageKey) === '1') return;
19087    } catch {}
19088    try {
19089      const data = await this.apiFetch('/api/lots/' + encodeURIComponent(cert) + '/views', {
19090        method: 'POST',
19091        body: JSON.stringify({
19092          sessionId: this.lotViewSessionId(),
19093          route: window.location.hash || '',
19094          referrer: document.referrer || '',
19095          screen: (window.screen?.width || 0) + 'x' + (window.screen?.height || 0),
19096          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
19097          qualifiedSeconds: 8,
19098          occurredAt: new Date().toISOString(),
19099        }),
19100      });
19101      try { localStorage.setItem(storageKey, '1'); } catch {}
19102      if (Number.isFinite(Number(data?.views))) {
19103        this.setState({ lotViewCounts: { ...this.state.lotViewCounts, [cert]: Number(data.views) } });
19104      }
19105    } catch (error) {
19106      if (![404, 405].includes(Number(error?.status))) console.warn('[Roslot lot view tracking failed]', { cert, message: error?.message });
19107    }
19108  }
19109  startLotViewTracking(cert) {
19110    if (this._viewT) clearTimeout(this._viewT);
19111    if (this._lotViewPoll) clearInterval(this._lotViewPoll);
19112    this.loadLotViewCounter(cert);
19113    this._viewT = setTimeout(() => {
19114      if (this.state.route === 'lot' && this.state.selectedCert === cert && !document.hidden) this.recordLotView(cert);
19115    }, 8000);
19116    this._lotViewPoll = setInterval(() => {
19117      if (this.state.route === 'lot' && this.state.selectedCert === cert && !document.hidden) this.loadLotViewCounter(cert, true);
19118    }, 20000);
19119  }
19120  async refreshRoslotPresence() {
19121    const uid = this.state.user?.uid;
19122    if (!uid || this.state.route !== 'pool' || document.hidden || this._roslotPresenceRequest) return;
19123    this._roslotPresenceRequest = true;
19124    if (!this.state.roslotPresence) this.setState({ roslotPresenceLoa
19124ding: true, roslotPresenceError: false });
19125    try {
19126      const now = Date.now();
19127      const heartbeatKey = 'roslot-presence-heartbeat:' + uid;
19128      let lastHeartbeat = Number(this._roslotPresenceHeartbeatAt || 0);
19129      try { lastHeartbeat = Math.max(lastHeartbeat, Number(localStorage.getItem(heartbeatKey)) || 0); } catch {}
19130      const heartbeatDue = !lastHeartbeat || now - lastHeartbeat >= 120000;
19131      const data = await this.apiFetch('/api/roslot/presence', heartbeatDue ?{ method: 'POST', body: '{}' } : { method: 'GET' });
19132      if (heartbeatDue) {
19133        this._roslotPresenceHeartbeatAt = now;
19134        try { localStorage.setItem(heartbeatKey, String(now)); } catch {}
19135      }
19136      this.setState({
19137        roslotPresence: {
19138          buyers: Math.max(0, Number(data?.buyers) || 0),
19139          sellers: Math.max(0, Number(data?.sellers) || 0),
19140          total: Math.max(0, Number(data?.total) || 0),
19141          updatedAt: data?.updatedAt || new Date().toISOString(),
19142        },
19143        roslotPresenceLoading: false,
19144        roslotPresenceError: false,
19145      });
19146    } catch (error) {
19147      if (![401, 403, 404].includes(Number(error?.status))) console.warn('[Roslot presence failed]', error?.message || error);
19148      this.setState({ roslotPresenceLoading: false, roslotPresenceError: true });
19149    } finally {
19150      this._roslotPresenceRequest = false;
19151    }
19152  }
19153  roslotLotPlayer() {
19154    return document.querySelector('video[data-lot-player]');
19155  }
19156  syncRoslotLotVideoState(video = this.roslotLotPlayer(), force = false) {
19157    if (!video) return;
19158    const now = Date.now();
19159    if (!force && this._roslotVideoStateAt && now - this._roslotVideoStateAt < 350) return;
19160    this._roslotVideoStateAt = now;
19161    const duration = Number.isFinite(video.duration) ?video.duration : 0;
19162    const progress = duration > 0 ?Math.min(100, Math.max(0, (Number(video.currentTime || 0) / duration) * 100)) : 0;
19163    this.setState({
19164      lotVideoPlaying: !video.paused && !video.ended,
19165      lotVideoProgress: progress,
19166      lotVideoDuration: duration,
19167      lotVideoVolume: video.muted ?0 : Number(video.volume || 0),
19168      lotVideoMuted: !!video.muted || Number(video.volume || 0) === 0,
19169    });
19170  }
19171  toggleRoslotLotVideoPlayback(event) {
19172    event?.preventDefault?.();
19173    const video = this.roslotLotPlayer();
19174    if (!video) return;
19175    if (video.paused) video.play?.().catch(() => {});
19176    else video.pause?.();
19177    this.syncRoslotLotVideoState(video, true);
19178  }
19179  seekRoslotLotVideo(event) {
19180    const video = this.roslotLotPlayer();
19181    if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return;
19182    const value = Math.max(0, Math.min(100, Number(event?.target?.value || 0)));
19183    video.currentTime = (value / 100) * video.duration;
19184    this.syncRoslotLotVideoState(video, true);
19185  }
19186  setRoslotLotVideoVolume(event) {
19187    const video = this.roslotLotPlayer();
19188    if (!video) return;
19189    const value = Math.max(0, Math.min(1, Number(event?.target?.value || 0)));
19190    try { video.volume = value; } catch (_) {}
19191    video.muted = value <= 0;
19192    this.syncRoslotLotVideoState(video, true);
19193  }
19194  toggleRoslotLotVideoMute(event) {
19195    event?.preventDefault?.();
19196    const video = this.roslotLotPlayer();
19197    if (!video) return;
19198    const nextMuted = !video.muted && Number(video.volume || 0) > 0;
19199    if (!nextMuted && Number(video.volume || 0) <= 0) {
19200      try { video.volume = 0.8; } catch (_) {}
19201    }
19202    video.muted = nextMuted;
19203    this.syncRoslotLotVideoState(video, true);
19204  }
19205  async openRoslotLotVideoFullscreen(event) {
19206    event?.preventDefault?.();
19207    const video = this.roslotLotPlayer();
19208    if (!video) return;
19209    const frame = video.closest('.dc-cabana-video-frame') || video;
19210    try {
19211      if (frame.requestFullscreen) await frame.requestFullscreen();
19212      else if (video.requestFullscreen) await video.requestFullscreen();
19213      else if (video.webkitEnterFullscreen) video.webkitEnterFullscreen();
19214    } catch (_) {}
19215  }
19216  openLot(cert, focusBid, options = {}) {
19217    if (!this.canAccessRoslot()) {
19218      this.showToast('Por ahora Roslot está disponible solo para administradores.');
19219      if (options.syncPath !== false) this.syncAppLocation('home', { replace: true });
19220      return;
19221    }
19222    const selectedCert = String(cert || '').trim();
19223    if (!selectedCert) return;
19224    if (options.syncPath !== false) this.syncRoslotLotLocation(selectedCert, { replace: options.replace === true });
19225    this.setState({ route: 'lot', selectedCert, selectedVideo: 0, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true, bidMessage: null });
19226    window.scrollTo(0, 0);
19227    if (this.state.lotsReal) this.fetchLotBids(selectedCert);
19228    const knownLot = (this.state.lots || []).find((lot) => lot.cert === selectedCert);
19229    if (knownLot) this.fetchLotSellerConsignataria(knownLot);
19230    const loading = !knownLot ? this.fetchRoslotLotByCert(selectedCert) : null;
19231    this.startLotViewTracking(selectedCert);
19232    return loading;
19233  }
19234  showToast(msg) { this.setState({ toast: msg }); clearTimeout(this._tt); this._tt = setTimeout(() => this.setState({ toast: '' }), 2200); }
19235  openRoslotMobilePanel() {
19236    if (!this.state.isMobile || this.state.route !== 'pool') return;
19237    this.setState({ roslotMobilePanelOpen: true });
19238  }
19239  closeRoslotMobilePanel() { this.setState({ roslotMobilePanelOpen: false }); }
19240  showRoslotLiveNotification(item) {
19241    if (!item || this.state.route !== 'pool') return;
19242    const pulseId = String(item.id || [item.type, item.createdAt, item.title].filter(Boolean).join(':'));
19243    clearTimeout(this._roslotLivePulseTimer);
19244    this.setState({ roslotLivePulseId: pulseId });
19245    this._roslotLivePulseTimer = setTimeout(() => this.setState({ roslotLivePulseId: '' }), 4500);
19246    if (!this.state.isMobile) return;
19247    clearTimeout(this._roslotLiveToastTimer);
19248    this.setState({ roslotLiveToast: item });
19249    this._roslotLiveToastTimer = setTimeout(() => this.setState({ roslotLiveToast: null }), 6500);
19250  }
19251  openRoslotLiveToast() {
19252    const item = this.state.roslotLiveToast;
19253    if (!item) return;
19254    clearTimeout(this._roslotLiveToastTimer);
19255    this.setState({ roslotLiveToast: null, roslotMobilePanelOpen: false });
19256    this.openNotification(item);
19257  }
19258  enterHome() { this.setState({ route: 'home', authMessage: '' }); window.scrollTo(0, 0); this.animateStats(); }
19259  focusAccountAssistantField(step = this.state.accountAssistantStep) {
19260    const fields = { 1: 'name', 2: 'phone', 3: 'email', 4: 'password', 5: 'submit' };
19261    const field = fields[Number(step)];
19262    if (!field) return;
19263    window.setTimeout(() => {
19264      const target = document.querySelector('[data-account-field="' + field + '"]');
19265      if (!target) return;
19266      try { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch {}
19267      if (field !== 'submit') {
19268        try { target.focus({ preventScroll: true }); } catch { try { target.focus(); } catch {} }
19269      }
19270    }, 100);
19271  }
19272  openAccountAssistant() {
19273    this.setState({
19274      accountAssistantOpen: true,
19275      accountAssistantMethod: '',
19276      accountAssistantStep: 0,
19277      accountAssistantMessage: '',
19278      authMode: 'signin',
19279      authMessage: '',
19280      authPasswordVisible: false,
19281    });
19282  }
19283  closeAccountAssistant() {
19284    this.setState({ accountAssistantOpen: false, accountAssistantMethod: '', accountAssistantStep: 0, accountAssistantMessage: '' });
19285  }
19286  chooseAccountAssistantGoogle() {
19287    this.setState({ accountAssistantMethod: 'google', accountAssistantStep: 0, accountAssistantMessage: '', authMode: 'signin', authMessage: '' });
19288  }
19289  chooseAccountAssistantEmail() {
19290    this.setState({ accountAssistantMethod: 'email', accountAssistantStep: 1, accountAssistantMessage: '', authMode: 'register', authMessage: '' }, () => this.focusAccountAssistantField(1));
19291  }
19292  backAccountAssistant() {
19293    const method = this.state.accountAssistantMethod;
19294    const step = Number(this.state.accountAssistantStep) || 0;
19295    if (method === 'google' || step <= 1) {
19296      this.setState({ accountAssistantMethod: '', accountAssistantStep: 0, accountAssistantMessage: '', authMode: 'signin', authMessage: '' });
19297      return;
19298    }
19299    const nextStep = step - 1;
19300    this.setState({ accountAssistantStep: nextStep, accountAssistantMessage: '', authMessage: '' }, () => this.focusAccountAssistantField(nextStep));
19301  }
19302  nextAccountAssistant() {
19303    const step = Number(this.state.accountAssistantStep) || 1;
19304    const email = String(this.state.formEmail || '').trim();
19305    const validations = {
19306      1: [!!String(this.state.formName || '').trim(), 'Escribí tu nombre y apellido para poder continuar.'],
19307      2: [!!this.phoneNumberE164(this.state.formPhone), 'Revisá el teléfono. Debe incluir el código de país, por ejemplo +54 9 341 000 0000.'],
19308      3: [/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), 'Escribí un correo válido, por ejemplo [email protected].'],
19309      4: [String(this.state.formPassword || '').length >= 8 && /[A-ZÁÉÍÓÚÜÑ]/.test(this.state.formPassword || ''), 'La contraseña necesita 8 caracteres o más y al menos una letra mayúscula.'],
19310    };
19311    if (validations[step] && !validations[step][0]) {
19312      this.setState({ accountAssistantMessage: validations[step][1] }, () => this.focusAccountAssistantField(step));
19313      return;
19314    }
19315    if (step >= 5) {
19316      this.setState({ accountAssistantMessage: '', authMessage: '' }, () => this.submitAuth());
19317      return;
19318    }
19319    const nextStep = step + 1;
19320    this.setState({ accountAssistantStep: nextStep, accountAssistantMessage: '', authMessage: '' }, () => this.focusAccountAssistantField(nextStep));
19321  }
19322  async submitAuth() {
19323    const { authMode, formEmail, formPassword, formName, formPhone } = this.state;
19324    const email = (formEmail || '').trim();
19325    const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
19326    if (authMode === 'reset') {
19327      if (!emailOk) { this.setState({ authMessage: 'Ingresá un email válido para recuperar la contraseña.' }); return; }
19328      if (this.state.authBusy) return;
19329      this.setState({ authBusy: true, authMessage: '' });
19330      try {
19331        await (await this.ensureFirebase()).auth().sendPasswordResetEmail(email, this.authActionSettings('password-reset'));
19332        this.setState({ authBusy: false, authMessage: 'Te enviamos el enlace para crear una nueva contraseña. Revisá también Spam o Correo no deseado.' });
19333      } catch (e) { this.setState({ authBusy: false, authMessage: this.authError(e) }); }
19334      return;
19335    }
19336    if (!emailOk) { this.setState({ authMessage: 'Ingresá un email válido.' }); return; }
19337    if (!formPassword || (authMode === 'register' && formPassword.length < 8) || (authMode !== 'register' && formPassword.length < 6)) { this.setState({ authMessage: authMode === 'register' ?'La contraseña debe tener al menos 8 caracteres.' : 'Ingresá tu contraseña.' }); return; }
19338    if (authMode === 'register' && !/[A-ZÁÉÍÓÚÜÑ]/.test(formPassword)) { this.setState({ authMessage: 'La contraseña debe incluir al menos una letra mayúscula.' }); return; }
19339    if (authMode === 'register' && !(formName || '').trim()) { this.setState({ authMessage: 'Ingresá tu nombre y apellido.' }); return; }
19340    if (authMode === 'register' && !this.phoneNumberE164(formPhone)) { this.setState({ authMessage: 'Ingresá tu teléfono con código de país, por ejemplo +54 9 341 000 0000.' }); return; }
19341    if (this.state.authBusy) return;
19342    this.setState({ authBusy: true, authMessage: '' });
19343    try {
19344      this.requestWelcomeAfterSignIn();
19345      if (authMode === 'register') {
19346        const firebase = await this.ensureFirebase();
19347        const cred = await firebase.auth().createUserWithEmailAndPassword(email, formPassword);
19348        if (formName) await cred.user.updateProfile({ displayName: formName.trim() });
19349        const persistedUser = await this.persistFirebaseUser(firebase, cred.user);
19350        this.setState({
19351          user: persistedUser,
19352          authBusy: false,
19353          emailVerificationRequired: false,
19354          emailVerificationMessage: '',
19355          phoneVerificationRequired: true,
19356          phoneVerificationStep: 'phone',
19357          phoneVerificationCode: '',
19358          phoneVerificationBusy: false,
19359          phoneVerificationMessage: '',
19360          phoneVerificationNumber: this.phoneNumberE164(formPhone) || '+54',
19361        });
19362        if (this._authHandling?.promise) await this._authHandling.promise;
19363        await this.savePhoneAndContinue();
19364      } else {
19365        const firebase = await this.ensureFirebase();
19366        const cred = await firebase.auth().signInWithEmailAndPassword(email, formPassword);
19367        const persistedUser = await this.persistFirebaseUser(firebase, cred.user);
19368        this.setState({ user: persistedUser, authBusy: false });
19369      }
19370    } catch (e) { this.clearWelcomeAfterSignIn(); this.setState({ authMessage: this.authError(e), authBusy: false }); }
19371  }
19372  loadScriptOnce(src, id) {
19373    if (window[id]) return Promise.resolve();
19374    const existing = document.querySelector('script[data-loader-id="' + id + '"]');
19375    if (existing) {
19376      if (existing.dataset.loaded === 'true') return Promise.resolve();
19377      return new Promise((resolve, reject) => {
19378        existing.addEventListener('load', () => { existing.dataset.loaded = 'true'; resolve(); }, { once: true });
19379        existing.addEventListener('error', reject, { once: true });
19380      });
19381    }
19382    return new Promise((resolve, reject) => {
19383      const el = document.createElement('script');
19384      el.src = src; el.async = true; el.dataset.loaderId = id;
19385      const timer = window.setTimeout(() => {
19386        el.remove();
19387        reject(new Error('Tiempo de espera agotado al cargar ' + src));
19388      }, 12000);
19389      el.onload = () => { window.clearTimeout(timer); el.dataset.loaded = 'true'; resolve(); };
19390      el.onerror = (error) => { window.clearTimeout(timer); reject(error); };
19391      document.head.appendChild(el);
19392    });
19393  }
19394  async ensureFirebase() {
19395    if (!window.firebase?.apps) await this.loadScriptOnce('/vendor/firebase-app-compat.js?v=10.12.5', 'firebase');
19396    if (!window.firebase?.auth) await this.loadScriptOnce('/vendor/firebase-auth-compat.js?v=10.12.5', 'firebase-auth-loaded');
19397    const firebase = window.firebase;
19398    if (!firebase.apps.length) firebase.initializeApp({
19399      apiKey: 'AIzaSyBKbNby76W3g8xR6iuueqy_dErZTSnS1RE',
19400      appId: '1:627421676271:web:2823188bcb07a97516911a',
19401      authDomain: window.location.hostname === 'rosgan.app' ? 'rosgan.app' : 'rosganapp.firebaseapp.com',
19402      messagingSenderId: '627421676271',
19403      projectId: 'rosganapp',
19404      storageBucket: 'rosganapp.firebasestorage.app',
19405    });
19406    const auth = firebase.auth();
19407    try { auth.useDeviceLanguage(); } catch {}
19408    // Firebase owns durable persistence and cross-tab sign-in/sign-out synchronization.
19409    // Do not downgrade to SESSION: it isolates authentication to a single tab.
19410    if (!this._firebasePersistencePromise) {
19411      this._firebasePersistencePromise = auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL)
19412        .then(() => 'local').catch((error) => { this._firebasePersistencePromise = null; throw error; });
19413    }
19414    const persistenceMode = await this._firebasePersistencePromise;
19415    try { document.documentElement.dataset.authPersistence = persistenceMode; } catch {}
19416    return firebase;
19417  }
19418  async persistFirebaseUser(firebase, user) {
19419    if (!user || this._signingOut) return null;
19420    const epoch = this._sessionEpoch || 0;
19421    const auth = firebase.auth();
19422    // Firebase sign-in already persists the user. Never replay an old credential
19423    // with updateCurrentUser: that can resurrect a session after sign-out.
19424    if (auth.currentUser?.uid !== user.uid) return null;
19425    await auth.currentUser.getIdToken();
19426    if (this._signingOut || epoch !== (this._sessionEpoch || 0) || auth.currentUser?.uid !== user.uid) return null;
19427    return auth.currentUser;
19428  }
19429  async notificationToken(firebase, registration) {
19430    if (!firebase.messaging) {
19431      await this.loadScriptOnce('/vendor/firebase-messaging-compat.js?v=10.12.5', 'firebase-messaging-loaded');
19432    }
19433    if (typeof firebase.messaging.isSupported === 'function') {
19434      const supported = await firebase.messaging.isSupported();
19435      if (!supported) {
19436        const error = new Error('Este navegador no es compatible con notificaciones push.');
19437        error.code = 'messaging/unsupported-browser';
19438        throw error;
19439      }
19440    }
19441    const messaging = firebase.messaging();
19442    if (!this._foregroundMessagingBound) {
19443      this._foregroundMessagingBound = true;
19444      messaging.onMessage((payload) => {
19445        const title = payload?.notification?.title || 'Roslot';
19446        const body = payload?.notification?.body || 'Tenes una nueva notificacion.';
19447        this.setState({ toast: `${title}: ${body}` });
19448        this.fetchNotifications(true);
19449        if (this.state.route === 'notifications') this.fetchNotificationHistory(true);
19450        if (document.hidden && Notification.permission === 'granted') {
19451          const notification = new Notification(title, {
19452            body,
19453            icon: 'assets/rosgan-logo-blue.png',
19454            tag: payload?.data?.type
19455              ? `${payload.data.type}-${payload?.data?.cert || 'account'}`
19456              : 'roslot-notification',
19457          });
19458          notification.onclick = () => {
19459            window.focus();
19460            if (payload?.data?.href) location.hash = payload.data.href.replace(/^#/, '');
19461            notification.close();
19462          };
19463        }
19464      });
19465    }
19466    const vapidKey = String(window.ROSGAN_FCM_VAPID_KEY || localStorage.getItem('rosgan.fcmVapidKey') || '').trim();
19467    return messaging.getToken({
19468      serviceWorkerRegistration: registration,
19469      ...(vapidKey ?{ vapidKey } : {}),
19470    });
19471  }
19472  async initializeNotifications() {
19473    if (!('Notification' in window) || !('serviceWorker' in navigator)) {
19474      this.setState({ notificationsStatus: 'unsupported' });
19475      return;
19476    }
19477    if (Notification.permission === 'denied') {
19478      this.setState({ notificationsStatus: 'denied' });
19479      return;
19480    }
19481    if (Notification.permission !== 'granted') {
19482      this.setState({ notificationsStatus: 'available' });
19483      return;
19484    }
19485    if (this._notificationsRegistering) return;
19486    this._notificationsRegistering = true;
19487    this.setState({ notificationsStatus: 'loading', notificationsMessage: '' });
19488    try {
19489      const firebase = await this.ensureFirebase();
19490      const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js?v=3', {
19491        scope: '/',
19492        updateViaCache: 'none',
19493      });
19494      await registration.update().catch(() => {});
19495      const token = await this.notificationToken(firebase, registration);
19496      if (!token) throw new Error('Firebase no devolvio un identificador para este navegador.');
19497      await this.apiFetch('/api/notifications/devices', {
19498        method: 'POST',
19499        body: JSON.stringify({
19500          token,
19501          platform: navigator.userAgentData?.platform || navigator.platform || '',
19502          userAgent: navigator.userAgent || '',
19503        }),
19504      });
19505      this._notificationToken = token;
19506      this.setState({ notificationsStatus: 'enabled', notificationsMessage: '' });
19507    } catch (error) {
19508      console.warn('notifications init', error);
19509      const unsupported = error?.code === 'messaging/unsupported-browser';
19510      this.setState({
19511        notificationsStatus: unsupported ?'unsupported' : 'error',
19512        notificationsMessage: error?.message || 'No se pudieron activar las notificaciones.',
19513      });
19514    } finally {
19515      this._notificationsRegistering = false;
19516    }
19517  }
19518  async enableNotifications() {
19519    if (!('Notification' in window)) {
19520      this.setState({ notificationsStatus: 'unsupported' });
19521      return;
19522    }
19523    const permission = await Notification.requestPermission();
19524    if (permission !== 'granted') {
19525      this.setState({ notificationsStatus: permission === 'denied' ?'denied' : 'available' });
19526      return;
19527    }
19528    await this.initializeNotifications();
19529  }
19530  async fetchWhatsAppNotificationPreference(silent = false) {
19531    if (!this.state.user || this.state.whatsappNotificationsLoading) return;
19532    this.setState({ whatsappNotificationsLoading: true, ...(!silent ?{ whatsappNotificationsMessage: '' } : {}) });
19533    try {
19534      const data = await this.apiFetch('/api/notifications/whatsapp/preferences');
19535      this.setState({
19536        whatsappNotificationsEnabled: data?.enabled === true,
19537        whatsappNotificationsAvailable: data?.available === true && data?.phoneVerified === true,
19538        whatsappNotificationsLoading: false,
19539        whatsappNotificationsMessage: '',
19540      });
19541    } catch (error) {
19542      this.setState({
19543        whatsappNotificationsLoading: false,
19544        whatsappNotificationsMessage: error?.message || 'No se pudo consultar WhatsApp.',
19545      });
19546    }
19547  }
19548  async toggleWhatsAppNotifications() {
19549    if (this.state.whatsappNotificationsSaving || this.state.whatsappNotificationsLoading) return;
19550    if (!this.state.whatsappNotificationsAvailable) {
19551      this.showToast('Verifica tu telefono para activar las notificaciones por WhatsApp.');
19552      return;
19553    }
19554    const enabled = !this.state.whatsappNotificationsEnabled;
19555    this.setState({ whatsappNotificationsSaving: true, whatsappNotificationsMessage: '' });
19556    try {
19557      const data = await this.apiFetch('/api/notifications/whatsapp/preferences', {
19558        method: 'PATCH',
19559        body: JSON.stringify({ enabled }),
19560      });
19561      this.setState({
19562        whatsappNotificationsEnabled: data?.enabled === true,
19563        whatsappNotificationsAvailable: data?.available === true && data?.phoneVerified === true,
19564        whatsappNotificationsSaving: false,
19565      });
19566      this.showToast(data?.enabled ?'Notificaciones por WhatsApp activadas.' : 'Notificaciones por WhatsApp desactivadas.');
19567    } catch (error) {
19568      this.setState({
19569        whatsappNotificationsSaving: false,
19570        whatsappNotificationsMessage: error?.message || 'No se pudo actualizar WhatsApp.',
19571      });
19572      this.showToast(error?.message || 'No se pudo actualizar WhatsApp.');
19573    }
19574  }
19575  async fetchNotifications(silent = false, includeRead = this.state.notificationsOpen) {
19576    if (!this.state.user || this._notificationsFetching) return;
19577    this._lastNotificationsFetchAt = Date.now();
19578    this._notificationsFetching = true;
19579    if (!silent) this.setState({ notificationsLoading: true, notificationsError: '' });
19580    try {
19581      const data = await this.apiFetch('/api/notifications?limit=50' + (includeRead ?'&includeRead=true' : ''));
19582      const items = Array.isArray(data?.items) ?data.items : [];
19583      const previousItems = this.state.notificationsItems;
19584      const notificationKey = (item) => String(item?.id || [item?.type, item?.createdAt, item?.title].filter(
19584Boolean).join(':'));
19585      const previousKeys = new Set((Array.isArray(previousItems) ?previousItems : []).map(notificationKey));
19586      const newRoslotItem = Array.isArray(previousItems)
19587        ?items.find((item) => String(item?.type || '').startsWith('roslot_') && !previousKeys.has(notificationKey(item)))
19588        : null;
19589      if (items.some(item => item.type === 'account_role_changed' && !previousKeys.has(notificationKey(item)))) this.refreshProfileData(true);
19590      const signature = JSON.stringify(items);
19591      if (silent && signature === this._notificationsSignature) return;
19592      this._notificationsSignature = signature;
19593      this.setState({
19594        notificationsItems: items,
19595        notificationsLoading: false,
19596        notificationsError: '',
19597      }, () => {
19598        if (newRoslotItem) this.showRoslotLiveNotification(newRoslotItem);
19599        if (this.state.notificationsOpen && items.some((item) => !item.read)) {
19600          this.markAllNotificationsRead({ preserveItems: true });
19601        }
19602      });
19603    } catch (error) {
19604      if (!silent) {
19605        this.setState({
19606          notificationsLoading: false,
19607          notificationsError: error?.message || 'No se pudieron cargar las notificaciones.',
19608        });
19609      }
19610    } finally {
19611      this._notificationsFetching = false;
19612    }
19613  }
19614  async fetchNotificationHistory(silent = false) {
19615    if (!this.state.user || this._notificationHistoryFetching) return;
19616    this._notificationHistoryFetching = true;
19617    const cacheKey = 'rosgan:notification-history:v1:' + String(this.state.user.uid || '');
19618    if (!Array.isArray(this.state.notificationHistory)) {
19619      const cached = this.readCollectionCache(cacheKey);
19620      if (Array.isArray(cached)) this.setState({ notificationHistory: cached, notificationHistoryError: '' });
19621    }
19622    if (!silent) this.setState({ notificationHistoryLoading: true, notificationHistoryError: '' });
19623    try {
19624      const data = await this.apiFetch('/api/notifications?limit=100&includeRead=true');
19625      const items = Array.isArray(data?.items) ?data.items : [];
19626      const signature = JSON.stringify(items);
19627      if (silent && signature === this._notificationHistorySignature) return;
19628      this._notificationHistorySignature = signature;
19629      this.writeCollectionCache(cacheKey, items, 30 * 60 * 1000);
19630      this.setState({
19631        notificationHistory: items,
19632        notificationHistoryLoading: false,
19633        notificationHistoryError: '',
19634      });
19635    } catch (error) {
19636      this.setState({
19637        notificationHistoryLoading: false,
19638        notificationHistoryError: error?.message || 'No se pudo cargar el historial.',
19639      });
19640    } finally {
19641      this._notificationHistoryFetching = false;
19642    }
19643  }
19644  goNotificationCenter() {
19645    this.setState({ notificationsOpen: false, roslotMobilePanelOpen: false });
19646    this.go('notifications');
19647    this.fetchNotificationHistory(false);
19648  }
19649  toggleNotifications() {
19650    const open = !this.state.notificationsOpen;
19651    this.setState({ notificationsOpen: open }, () => {
19652      if (!open) return;
19653      this.markAllNotificationsRead({ preserveItems: true, force: true });
19654      this.fetchNotifications(false, true);
19655    });
19656  }
19657  notificationDestination(item) {
19658    const href = String(item?.href || '').trim();
19659    if (item?.type !== 'remate_lot_workflow') return href;
19660    // Old inbox entries use a hash; state-change alerts always open the auction list.
19661    const match = href.match(/^(?:#)?\/remates\/([^/?#]+)(?:\/auditor)?(?:[?#]|$)/);
19662    if (!match) return href;
19663    return '/remates/' + match[1] + '/auditor';
19664  }
19665  async openNotification(item) {
19666    if (!item) return;
19667    if (!item.read) {
19668      this.setState({
19669        notificationsItems: (this.state.notificationsItems || []).filter((notification) => notification.id !== item.id),
19670        notificationHistory: Array.isArray(this.state.notificationHistory)
19671          ? this.state.notificationHistory.map((notification) => (
19672            notification.id === item.id ?{ ...notification, read: true } : notification
19673          ))
19674          : this.state.notificationHistory,
19675      });
19676      this.apiFetch('/api/notifications/' + encodeURIComponent(item.id) + '/read', { method: 'PATCH' })
19677        .catch(() => this.fetchNotifications(true));
19678    }
19679    this.setState({ notificationsOpen: false, roslotMobilePanelOpen: false });
19680    const destination = this.notificationDestination(item);
19681    if (destination) {
19682      const href = destination;
19683      if (href.startsWith('#')) {
19684        this.navigateFromHash(href);
19685        return;
19686      }
19687      try {
19688        const target = new URL(href, window.location.origin);
19689        if (target.origin !== window.location.origin) {
19690          window.location.assign(target.href);
19691          return;
19692        }
19693        const nextUrl = target.pathname + target.search + target.hash;
19694        this.writeAppHistory(nextUrl);
19695        this.navigateFromPath(target.pathname);
19696      } catch {
19697        window.location.assign(href);
19698      }
19699    }
19700  }
19701  async markAllNotificationsRead({ preserveItems = false, force = false } = {}) {
19702    const items = this.state.notificationsItems || [];
19703    if (!force && !items.some((item) => !item.read)) return;
19704    this.setState({
19705      notificationsItems: preserveItems
19706        ?items.map((item) => ({ ...item, read: true }))
19707        : [],
19708      notificationHistory: Array.isArray(this.state.notificationHistory)
19709        ?this.state.notificationHistory.map((item) => ({ ...item, read: true }))
19710        : this.state.notificationHistory,
19711    });
19712    if (this._markAllNotificationsReadPromise) return this._markAllNotificationsReadPromise;
19713    this._markAllNotificationsReadPromise = this.apiFetch('/api/notifications/read-all', { method: 'PATCH' });
19714    try {
19715      await this._markAllNotificationsReadPromise;
19716    } catch {
19717      this.fetchNotifications(true, this.state.notificationsOpen);
19718    } finally {
19719      this._markAllNotificationsReadPromise = null;
19720    }
19721  }
19722  async syncAuthUser(user) {
19723    try {
19724      const token = await user.getIdToken();
19725      const response = await fetch('/api/auth/sync-user', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ displayName: user.displayName || '', email: user.email || '', phoneNumber: user.phoneNumber || '', phoneVerified: !!user.phoneNumber }) });
19726      const data = await response.json().catch(() => null);
19727      if (!response.ok) throw new Error(data?.message || 'No se pudo sincronizar el usuario.');
19728      if (data?.session?.apiError) console.warn('sync user api', data.session.apiError);
19729      this.setState({ authSyncError: '' });
19730    } catch (e) { console.warn('sync user', e); this.setState({ authSyncError: e?.message || 'No se pudo sincronizar tu usuario con Rosgan API.' }); }
19731  }
19732  authError(e) {
19733    const c = e?.code || '';
19734    if (c.includes('invalid-credential') || c.includes('wrong-password') || c.includes('user-not-found')) return 'Email o contraseña incorrectos.';
19735    if (c.includes('invalid-email')) return 'El email no es válido.';
19736    if (c.includes('email-already-in-use')) return 'Ya existe una cuenta con ese email.';
19737    if (c.includes('weak-password')) return 'La contraseña debe tener al menos 8 caracteres e incluir una letra mayúscula.';
19738    if (c.includes('expired-action-code')) return 'El enlace venció. Solicitá uno nuevo.';
19739    if (c.includes('invalid-action-code')) return 'El enlace no es válido o ya fue utilizado. Solicitá uno nuevo.';
19740    if (c.includes('too-many-requests')) return 'Demasiados intentos. Esperá unos minutos e intentá de nuevo.';
19741    if (c.includes('network')) return 'Sin conexión. Revisá tu internet e intentá de nuevo.';
19742    if (c.includes('operation-not-allowed')) return 'Este método de acceso no está habilitado. Contactá a Rosgan.';
19743    return 'No pudimos procesar la solicitud. Probá de nuevo.';
19744  }
19745  async googleAuth() {
19746    if (this.state.authBusy) return;
19747    try {
19748      const firebase = await this.ensureFirebase();
19749      const auth = firebase.auth();
19750      const provider = new firebase.auth.GoogleAuthProvider();
19751      provider.setCustomParameters({ prompt: 'select_account' });
19752      this.setState({ authBusy: true, authMessage: 'Abriendo Google...' });
19753      this.requestWelcomeAfterSignIn();
19754      const prefersRedirect = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || '')
19755        && window.location.hostname === 'rosgan.app';
19756      if (prefersRedirect) {
19757        this.setState({ authMessage: 'Continuando con Google…' });
19758        await auth.signInWithRedirect(provider);
19759        return;
19760      }
19761      try {
19762        const credential = await auth.signInWithPopup(provider);
19763        if (!credential?.user) throw new Error('Google no devolvió una sesión válida.');
19764        const persistedUser = await this.persistFirebaseUser(firebase, credential.user);
19765        this.setState({ user: persistedUser, authBusy: false, authMessage: '' });
19766        return;
19767      } catch (popupError) {
19768        const code = String(popupError?.code || '');
19769        if (!code.includes('popup-blocked') && !code.includes('operation-not-supported-in-this-environment')) throw popupError;
19770        this.setState({ authMessage: 'El navegador bloqueó la ventana. Continuando con Google…' });
19771        await auth.signInWithRedirect(provider);
19772      }
19773    } catch (e) {
19774      this.clearWelcomeAfterSignIn();
19775      if (String(e?.code || '').includes('account-exists-with-different-credential')) {
19776        const email = String(e?.email || this.state.formEmail || '').trim().toLowerCase();
19777        this.setState({
19778          authBusy: false,
19779          authMode: 'signin',
19780          formEmail: email,
19781          formPassword: '',
19782          authMessage: 'Esta cuenta utiliza acceso manual. Ingresá con el email y la contraseña; no uses el botón de Google. Si no recordás la contraseña, usá “Olvidé mi contraseña”.',
19783        });
19784        return;
19785      }
19786      this.setState({ authBusy: false, authMessage: this.googleErr(e) });
19787    }
19788  }
19789  googleErr(e) {
19790    const c = e?.code || '';
19791    const raw = e?.message ?' (' + c + ')' : (c ?' (' + c + ')' : '');
19792    if (c.includes('unauthorized-domain')) return 'Este dominio no está autorizado en Firebase. Agregá ' + window.location.hostname + ' en Authentication → Settings → Authorized domains.';
19793    if (c.includes('account-exists-with-different-credential')) return 'Esta cuenta utiliza acceso manual. Ingresá con el email y la contraseña.';
19794    if (c.includes('operation-not-allowed')) return 'Google Sign-In no está habilitado en el proyecto Firebase.';
19795    if (c.includes('popup-closed')) return 'Se cerró la ventana de Google antes de completar el ingreso.';
19796    if (c.includes('popup')) return 'El navegador bloqueó la ventana de Google. Permití pop-ups y tocá nuevamente Continuar con Google.';
19797    if (c.includes('network-request-failed') || c.includes('network')) return 'Google no pudo conectarse. Revisá la conexión y volvé a intentarlo.';
19798    if (c.includes('app-compat/no-app') || c.includes('no-app')) return 'Firebase todavía no terminó de inicializar. Recargá la página e intentá de nuevo.';
19799    return 'No pudimos conectar con Google' + raw + '. Probá de nuevo o usá email.';
19800  }
19801  ytId(url) { return (url || '').match(/(?:v=|youtu\.be\/|\/vi\/|embed\/|shorts\/)([\w-]{11})/)?.[1] || ''; }
19802  consKey(v) {
19803    const key = (v || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, '');
19804    return key === 'todos' ?'rosgan' : key;
19805  }
19806  consDisplayName(value) {
19807    const name = String(value || '').trim();
19808    return this.consKey(name) === 'rosgan' ?'Rosgan' : name;
19809  }
19810  isExcludedConsignataria(value) {
19811    const key = this.consKey(value);
19812    return key.includes('gananorpujol') || key.includes('reggi');
19813  }
19814  isExcludedRemate(item) {
19815    // Entrada importada de agenda que duplica el remate de octubre vigente (API Remates, ID 5).
19816    const legacyId = String(item?.sourceId || item?.id_remate || item?.id || '');
19817    if ((legacyId === '1854' || legacyId === 'rosgan-1854')
19818      && String(item?.fecha_remate || '').slice(0, 10) === '2026-10-08'
19819      && this.consKey(item?.nombre_remate) === 'rematehabitualoctubre') return true;
19820    return [item?.consignatariaId, item?.consignatariaNombre, item?.consignataria, item?.nombre_cuenta, item?.nombre_consignatario, item?.nombre_remate]
19821      .some((value) => this.isExcludedConsignataria(value));
19822  }
19823  isRosganHistoricalRemate(item) {
19824    if (!item || this.isExcludedRemate(item)) return false;
19825    const typeAndName = this.consKey([item?.nombre_tipo, item?.tipo_remate, item?.nombre_remate].filter(Boolean).join(' '));
19826    if (this.remateType(item)?.[0] === 'Cabañas' || typeAndName.includes('cabana')) return false;
19827    const dateKey = String(item?.fecha_remate || item?.fecha || '').slice(0, 10);
19828    return !dateKey || dateKey <= '2026-09-04';
19829  }
19830  consLocalBrand(name) {
19831    const record = this.consRecord(name);
19832    if ([name,record?.name,record?.nombre,record?.slug,record?.id].some(value=>this.consKey(value).includes('ganaderosdeceres'))) return {
19833      color:'#36A9E1',rgb:'rgb(54,169,225)',logo:'assets/consignatarias/ganaderos-de-ceres-celeste.svg',
19834      whiteLogo:'assets/consignatarias-white/ganaderos-de-ceres-coop-ltda.png',
19835      fullLogo:'assets/consignatarias-full-white/ganaderos-de-ceres-coop-ltda.png',
19836    };
19837    const matches = [name, record?.name, record?.nombre, record?.slug, record?.id]
19838      .some(value => /unionagricola(de)?avellaneda|coop(de)?avellaneda/.test(this.consKey(value)));
19839    if (!matches) return null;
19840    return {
19841      color: '#70292D', rgb: 'rgb(112,41,45)',
19842      logo: 'assets/consignatarias/coop-union-agricola-avellaneda.png',
19843      whiteLogo: 'assets/consignatarias-white/coop-union-agricola-avellaneda.png',
19844      fullLogo: 'assets/consignatarias-full-white/coop-union-agricola-avellaneda.png',
19845    };
19846  }
19847  consLogo(name) {
19848    const key = this.consKey(name); if (!key) return '';
19849    if (key === 'rosgan') return 'assets/brand/rosgan-iso-blue.png';
19850    const brand = this.consLocalBrand(name);
19851    if (brand) return brand.logo;
19852    const list = this.state.consignatarias || [];
19853    const hit = list.find((c) => [c.name, c.nombre, c.slug, c.id].some((v) => this.consKey(v) === key))
19854      || list.find((c) => { const k = this.consKey(c.name || c.nombre); return k && (k.includes(key) || key.includes(k)); });
19855    return hit?.logoUrl || hit?.logo || hit?.imageUrl || '';
19856  }
19857  consFullHeroLogo(name) {
19858    const brand = this.consLocalBrand(name);
19859    if (brand) return brand.fullLogo;
19860    const key = this.consKey(name);
19861    const logos = {
19862      aguirrevazquezsa: 'aguirre-vazquez-s-a.png', aguirrevazquez: 'aguirre-vazquez-s-a.png',
19863      alzagaunzueyciasa: 'alzaga-unzue-y-cia-s-a.png', alzagaunzue: 'alzaga-unzue-y-cia-s-a.png',
19864      colomboycolombosa: 'colombo-y-colombo-s-a.png', colomboycolombo: 'colombo-y-colombo-s-a.png',
19865      coopguillermolehmann: 'coop-guillermo-lehmann.png', guillermolehmann: 'coop-guillermo-lehmann.png', lehmann: 'coop-guillermo-lehmann.png',
19866      edgarepastoreyciasrl: 'edgar-e-pastore-y-cia-s-r-l.png', edgarepastore: 'edgar-e-pastore-y-cia-s-r-l.png', pastore: 'edgar-e-pastore-y-cia-s-r-l.png',
19867      etchevehereruralsrl: 'etchevehere-rural-s-r-l.png', etchevehere: 'etchevehere-rural-s-r-l.png',
19868      ferialvarezsrl: 'ferialvarez-s-r-l.png', ferialvarez: 'ferialvarez-s-r-l.png',
19869      ganaderosdecerescoopltda: 'ganaderos-de-ceres-coop-ltda.png', ganaderosdeceres: 'ganaderos-de-ceres-coop-ltda.png',
19870      coopldtaunionagricoladeavellaneda: 'coop-union-agricola-avellaneda.png', coopunionagricoladeavellaneda: 'coop-union-agricola-avellaneda.png', unionagricoladeavellaneda: 'coop-union-agricola-avellaneda.png', coopdeavellaneda: 'coop-union-agricola-avellaneda.png',
19871      ildarrazhnossa: 'ildarraz-hnos-s-a.png', ildarraz: 'ildarraz-hnos-s-a.png',
19872      rigran: 'rigran.png',
19873    };
19874    const file = logos[key] || Object.entries(logos).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1];
19875    return file ?'assets/consignatarias-full-white/' + file : '';
19876  }
19877  consFullHeroLogoFilter(name) {
19878    return /lehmann/.test(this.consKey(name)) ?'brightness(0) invert(1)' : 'none';
19879  }
19880  consRecord(name) {
19881    const key = this.consKey(name);
19882    if (!key) return null;
19883    const list = this.state.consignatarias || [];
19884    return list.find((c) => [c.name, c.nombre, c.slug, c.id].some((v) => this.consKey(v) === key))
19885      || list.find((c) => { const candidate = this.consKey(c.name || c.nombre); return candidate && (candidate.includes(key) || key.includes(candidate)); })
19886      || null;
19887  }
19888  consBackground(name) {
19889    const record = this.consRecord(name);
19890    if (record?.backgroundUrl || record?.bannerUrl) return record.backgroundUrl || record.bannerUrl;
19891    const key = this.consKey(name);
19892    const slugs = {
19893      alzagaunzueyciasa: 'alzaga-unzue-y-cia-s-a', alzagaunzue: 'alzaga-unzue-y-cia-s-a',
19894      ildarrazhnossa: 'ildarraz-hnos-s-a', ildarraz: 'ildarraz-hnos-s-a',
19895      aguirrevazquezsa: 'aguirre-vazquez-s-a', aguirrevazquez: 'aguirre-vazquez-s-a',
19896      colomboycolombosa: 'colombo-y-colombo-s-a', colomboycolombo: 'colombo-y-colombo-s-a',
19897      coopguillermolehmann: 'coop-guillermo-lehmann', guillermolehmann: 'coop-guillermo-lehmann', coopglehmann: 'coop-guillermo-lehmann', cooplehmann: 'coop-guillermo-lehmann', lehmann: 'coop-guillermo-lehmann',
19898      edgarepastoreyciasrl: 'edgar-e-pastore-y-cia-s-r-l', edgarepastore: 'edgar-e-pastore-y-cia-s-r-l', pastore: 'edgar-e-pastore-y-cia-s-r-l',
19899      etchevehereruralsrl: 'etchevehere-rural-s-r-l', etchevehere: 'etchevehere-rural-s-r-l',
19900      ferialvarezsrl: 'ferialvarez-s-r-l', ferialvarez: 'ferialvarez-s-r-l',
19901      ganaderosdecerescoopltda: 'ganaderos-de-ceres-coop-ltda', ganaderosdeceres: 'ganaderos-de-ceres-coop-ltda',
19902      rigran: 'rigran',
19903    };
19904    const slug = slugs[key] || Object.entries(slugs).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1];
19905    return slug ?'/api/consignatarias/' + slug + '/background' : '';
19906  }
19907  consBackgroundPosition(name, usage = 'banner') {
19908    const record = this.consRecord(name);
19909    const stored = record?.backgroundPosition || record?.bannerPosition;
19910    if (stored && usage === 'banner') return stored;
19911    const key = this.consKey(name);
19912    const positions = {
19913      aguirrevazquezsa: { banner: 'center 55%', card: 'center 55%' },
19914      aguirrevazquez: { banner: 'center 55%', card: 'center 55%' },
19915      alzagaunzueyciasa: { banner: 'center top', card: 'center 38%' },
19916      alzagaunzue: { banner: 'center top', card: 'center 38%' },
19917      colomboycolombosa: { banner: 'center 45%', card: 'center 48%' },
19918      colomboycolombo: { banner: 'center 45%', card: 'center 48%' },
19919      etchevehereruralsrl: { banner: 'center 40%', card: 'center 44%' },
19920      etchevehere: { banner: 'center 40%', card: 'center 44%' },
19921      ferialvarezsrl: { banner: 'center 40%', card: 'center 47%' },
19922      ferialvarez: { banner: 'center 40%', card: 'center 47%' },
19923      ildarrazhnossa: { banner: 'center 40%', card: 'center 46%' },
19924      ildarraz: { banner: 'center 40%', card: 'center 46%' },
19925      coopguillermolehmann: { banner: 'center 22%', card: 'center 45%' },
19926      guillermolehmann: { banner: 'center 22%', card: 'center 45%' },
19927      lehmann: { banner: 'center 22%', card: 'center 45%' },
19928      edgarepastoreyciasrl: { banner: 'center 28%', card: 'center 43%' },
19929      edgarepastore: { banner: 'center 28%', card: 'center 43%' },
19930      pastore: { banner: 'center 28%', card: 'center 43%' },
19931      ganaderosdecerescoopltda: { banner: 'center 45%', card: 'center 48%' },
19932      ganaderosdeceres: { banner: 'center 45%', card: 'center 48%' },
19933      rigran: { banner: 'center 30%', card: 'center 49%' },
19934    };
19935    const match = positions[key] || Object.entries(positions).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1];
19936    return match?.[usage] || match?.banner || stored || 'center';
19937  }
19938  remateConsignatariaRef(item) {
19939    return item?.consignataria || item?.nombre_cuenta || item?.nombre_consignatario || item?.consignatariaName || item?.consignatariaId || '';
19940  }
19941  feriaState(d) { if (!d) return 'proximo'; const now = new Date(); const diff = d - now; if (diff > 0) return 'proximo'; if (diff > -3 * 3600e3) return 'vivo'; return 'anterior'; }
19942  feriaFavoritesFor(consignataria) {
19943    // Plazas de ferias relevadas en Rosgan BCR (2026). No incluye especiales de cabañas.
19944    const groups = [
19945      ['aguirre', [['Alcorta','Santa Fe'],['Arrecifes','Buenos Aires'],['General Alvear','Mendoza'],['Lincoln','Buenos Aires']]],
19946      ['ganaderosdeceres', [['Ceres','Santa Fe'],['Hersilia','Santa Fe'],['San Cristóbal','Santa Fe'],['Suardi','Santa Fe'],['Tostado','Santa Fe'],['Vera','Santa Fe'],['Malbrán','Santiago del Estero'],['Quimilí','Santiago del Estero'],['General Pinedo','Chaco']]],
19947      ['ildarraz', [['Federal','Entre Ríos','Sociedad Rural'],['San Jaime de la Frontera','Entre Ríos','Don Polo II'],['Ceibas','Entre Ríos','Las Achiras']]],
19948      ['etchevehere', [['General Ramírez','Entre Ríos','María Dolores'],['Federal','Entre Ríos','Sociedad Rural']]],
19949      ['pastore', [['Cañada Rosquín','Santa Fe'],['Santa Regina','Buenos Aires'],['Rosario','Santa Fe','Mercado de Ganado de Rosario']]],
19950      ['rigran', [['San Justo','Santa Fe','Sociedad Rural'],['Reconquista','Santa Fe','Sociedad Rural'],['Goya','Corrientes','Sociedad Rural'],['Esquina','Corrientes','Sociedad Rural']]],
19951      ['ferialvarez', [['Río Cuarto','Córdoba','Predio Ferialvarez']]],
19952    ];
19953    const key = this.consKey(consignataria);
19954    const group = groups.find(([match]) => key.includes(match));
19955    return (group?.[1] || []).map(([localidad, provincia, predio]) => ({
19956      id: this.consKey(localidad), localidad, provincia, predio:predio || '',
19957      mapSrc: localidad === 'Rosario' ? '/assets/feria-localidades/mercado-rosario.svg' : '/assets/feria-localidades/' + this.consKey(provincia) + '-' + this.consKey(localidad) + '.svg',
19958      label: predio ?localidad + ' · ' + predio : localidad,
19959      nombre: predio === 'Mercado de Ganado de Rosario' ? predio : 'Feria ' + (predio === 'María Dolores' ?predio : localidad),
19960    }));
19961  }
19962  chooseFeriaFavorite(id) {
19963    const favorite = this.feriaFavoritesFor(this.state.feriaForm.consignataria).find(item => item.id === id);
19964    if (!favorite) return;
19965    clearTimeout(this._feriaPlaceTimer);
19966    this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1;
19967    const ff = { ...this.state.feriaForm, provincia:favorite.provincia, localidad:favorite.localidad, lat:'', lon:'' };
19968    ff.nombre = favorite.nombre;
19969    this.setState({ feriaForm:ff, feriaTitleAuto:true, feriaManualOpen:false, feriaFavoriteId:id, feriaPlaceResults:[], feriaPlaceLoading:false, feriaPlaceError:'', feriaFormMessage:null });
19970  }
19971  clearFeriaFavorite() {
19972    this.setFF('provincia', '');
19973    this.setState({ feriaManualOpen:true });
19974  }
19975  chooseFeriaConsignataria(name, logo) {
19976    if (!this.canManageUsers(this.state.apiUser)) return;
19977    if (this.state.feriaForm.consignataria === name) return;
19978    clearTimeout(this._feriaPlaceTimer);
19979    this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1;
19980    this.setState({ feriaForm:{ ...this.state.feriaForm, consignataria:name, consignatariaLogo:logo, provincia:'', localidad:'', nombre:'', lat:'', lon:'' }, feriaTitleAuto:true, feriaManualOpen:false, feriaFavoriteId:'', feriaPlaceResults:[], feriaPlaceLoading:false, feriaPlaceError:'', feriaFormMessage:null }, () => { if (logo) this.extractLogoColor(name, logo); });
19981  }
19982  setFF(key, val) {
19983    const ff = { ...this.state.feriaForm, [key]: val };
19984    if (key === 'localidad') {
19985      if (this.state.feriaTitleAuto || !ff.nombre) ff.nombre = val ?'Feria ' + val : '';
19986      ff.lat = ''; ff.lon = '';
19987    }
19988    const patch = { feriaForm: ff, feriaFormMessage: null };
19989    if (key === 'provincia' || key === 'localidad') {
19990      patch.feriaFavoriteId = '';
19991      clearTimeout(this._feriaPlaceTimer);
19992      this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1;
19993      patch.feriaPlaceResults = []; patch.feriaPlaceLoading = false;
19994    }
19995    if (key === 'nombre') patch.feriaTitleAuto = false;
19996    if (key === 'provincia') {
19997      clearTimeout(this._feriaPlaceTimer);
19998      this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1;
19999      ff.localidad = ''; ff.lat = ''; ff.lon = '';
20000      if (this.state.feriaTitleAuto) ff.nombre = '';
20001      patch.feriaPlaceLoading = false; patch.feriaPlaceResults = []; patch.feriaPlaceError = '';
20002    }
20003    if (key === 'consignataria') { const lg = this.consLogo(val); if (lg) this.extractLogoColor(val, lg); }
20004    this.setState(patch);
20005  }
20006  onFeriaPlaceInput(value) {
20007    const localidad = String(value || '');
20008    this.setFF('localidad', localidad);
20009    clearTimeout(this._feriaPlaceTimer);
20010    if (!this.state.feriaForm.provincia || localidad.trim().length < 3) {
20011      this.setState({ feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '' });
20012      return;
20013    }
20014    const provincia = this.state.feriaForm.provincia;
20015    this._feriaPlaceTimer = setTimeout(() => this.searchFeriaPlaces(localidad, provincia), 280);
20016  }
20017  async searchFeriaPlaces(localidad, provincia) {
20018    const query = String(localidad || '').trim();
20019    if (!provincia || query.length < 3) return;
20020    const token = (this._feriaPlaceToken || 0) + 1;
20021    this._feriaPlaceToken = token;
20022    this.setState({ feriaPlaceLoading: true, feriaPlaceError: '' });
20023    try {
20024      const response = await this.apiFetch('/api/places/search', { method: 'POST', body: JSON.stringify({ query, provincia: String(provincia).trim() }) });
20025      if (token !== this._feriaPlaceToken) return;
20026      const results = Array.isArray(response?.data) ?response.data : this.listFrom(response, ['data', 'items', 'results']);
20027      this.setState({ feriaPlaceLoading: false, feriaPlaceResults: results.slice(0, 7), feriaPlaceError: results.length ?'' : 'No encontramos esa localidad. Podés escribirla manualmente.' });
20028    } catch {
20029      if (token !== this._feriaPlaceToken) return;
20030      this.setState({ feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: 'Google no respondió. Podés escribir la localidad manualmente.' });
20031    }
20032  }
20033  selectFeriaPlace(item) {
20034    if (!item) return;
20035    const localidad = String(item.label || item.address || '');
20036    const ff = { ...this.state.feriaForm, localidad, lat: item.lat != null ?String(item.lat) : '', lon: item.lng != null ?String(item.lng) : '' };
20037    if (this.state.feriaTitleAuto || !ff.nombre) ff.nombre = localidad ?'Feria ' + localidad : '';
20038    this.setState({ feriaForm: ff, feriaFavoriteId:'', feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '', feriaFormMessage: null });
20039  }
20040  closeFeriaPlaceResults() {
20041    setTimeout(() => this.setState({ feriaPlaceResults: [] }), 150);
20042  }
20043  async fetchProvinces() {
20044    if (this.state.geoProvinces || this.state.geoLoadingProv) return;
20045    this.setState({ geoLoadingProv: true });
20046    try {
20047      const r = await fetch('/api/georef/provincias');
20048      const j = await r.json();
20049      if (!r.ok) throw new Error(j?.message || 'No se pudieron obtener las provincias.');
20050      const provs = (j.items || []).map((p) => p.nombre).sort((a, b) => a.localeCompare(b, 'es'));
20051      this.setState({ geoProvinces: provs, geoLoadingProv: false });
20052    } catch { this.setState({ geoProvinces: [...ARGENTINA_PROVINCES], geoLoadingProv: false }); }
20053  }
20054  async fetchLocalities(prov) {
20055    if (!prov || this.state.geoLocs[prov]) return;
20056    this.setState({ geoLoadingLoc: true });
20057    try {
20058      const r = await fetch('/api/georef/localidades?provincia=' + encodeURIComponent(prov));
20059      const j = await r.json();
20060      if (!r.ok) throw new Error(j?.message || 'No se pudieron obtener las localidades.');
20061      const locs = (j.items || []).map((l) => ({ nombre: l.nombre, lat: l.lat, lon: l.lon })).sort((a, b) => a.nombre.localeCompare(b.nombre, 'es'));
20062      this.setState({ geoLocs: { ...this.state.geoLocs, [prov]: locs }, geoLoadingLoc: false });
20063    } catch { this.setState({ geoLoadingLoc: false }); }
20064  }
20065  feriaAssignedConsignataria() {
20066    if (this.canManageUsers(this.state.apiUser)) return null;
20067    const refs = (this.state.userConsignatarias || []).map((item) =>
20067 item?.ref || item?.id || item?.slug || item?.name).filter(Boolean);
20068    const primary = this.state.userConsignataria;
20069    refs.push(primary?.ref || primary?.id || primary?.slug || primary?.name || '');
20070    const match = (this.state.consignatarias || []).find((item) => {
20071      const values = [item?.id, item?.slug, item?.name, item?.nombre].filter(Boolean);
20072      return refs.some((ref) => values.some((value) => this.consKey(value) === this.consKey(ref)));
20073    });
20074    if (match) return { ...match, name: this.consName(match), logoUrl: this.consLogo(this.consName(match)) || match.logoUrl || match.logo || match.imageUrl };
20075    const fallback = refs.find(Boolean);
20076    return fallback ?{ id: fallback, slug: fallback, name: fallback, logoUrl: this.consLogo(fallback) } : null;
20077  }
20078  async prepareFeriaConsignataria() {
20079    if (this.canManageUsers(this.state.apiUser)) return;
20080    if (!this.state.userConsignataria && this.state.user?.uid) await this.fetchUserConsignataria(this.state.user.uid);
20081    const assigned = this.feriaAssignedConsignataria();
20082    if (!assigned) {
20083      this.setState({ feriaFormMessage: { text: 'Tu usuario no tiene una consignataria asignada.', ok: false } });
20084      return;
20085    }
20086    const name = this.consName(assigned) || assigned.name;
20087    const logo = this.consLogo(name) || assigned.logoUrl;
20088    this.setState({ feriaForm: { ...this.state.feriaForm, consignataria: name, consignatariaLogo: logo }, feriaFormMessage: null });
20089    if (logo) this.extractLogoColor(name, logo);
20090  }
20091  feriaValid(form = this.state.feriaForm) { const f = form || {}; return !!(f.consignataria && f.provincia && f.localidad && f.fecha && f.hora && f.cabezas && Number(f.cabezas) > 0); }
20092  editSavedFeria(f) {
20093    this.go('feria');
20094    this.setState({feriaEditingId:f.id,feriaPhase:'idle',feriaResult:null,feriaError:'',feriaTitleAuto:false,
20095      feriaForm:{consignataria:f.consignataria,consignatariaLogo:this.consLogo(f.consignataria),provincia:f.provincia||'',localidad:f.localidad||'',nombre:f.nombreRemate||'',fecha:String(f.fecha||'').slice(0,10),hora:String(f.hora||'').slice(0,5),cabezas:f.cabezas,youtube:f.youtubeUrl||'',lat:f.latitud||'',lon:f.longitud||''}});
20096  }
20097  async submitFeria() {
20098    if (this.state.feriaPhase === 'connecting' || this.state.feriaPhase === 'creating') return;
20099    let f = this.state.feriaForm;
20100    if (!this.canManageUsers(this.state.apiUser)) {
20101      const assigned = this.feriaAssignedConsignataria();
20102      if (!assigned) { this.setState({ feriaFormMessage: { text: 'Tu usuario no tiene una consignataria asignada.', ok: false } }); return; }
20103      const assignedName = this.consName(assigned) || assigned.name;
20104      f = { ...f, consignataria: assignedName, consignatariaLogo: assigned.logoUrl || this.consLogo(assignedName) };
20105      this.setState({ feriaForm: f });
20106    }
20107    if (!this.feriaValid(f)) { this.setState({ feriaFormMessage: { text: 'Completá consignataria, provincia, localidad, fecha, hora y cabezas.', ok: false } }); return; }
20108    this.setState({ feriaPhase: 'connecting', feriaError: '', feriaResult: null, feriaFormMessage: null });
20109    window.scrollTo(0, 0);
20110    setTimeout(() => { if (this.state.feriaPhase === 'connecting') this.setState({ feriaPhase: 'creating' }); }, 1100);
20111    const iso = new Date(f.fecha + 'T' + f.hora).toISOString();
20112    const payload = { consignataria: f.consignataria, provincia: f.provincia, localidad: f.localidad, nombreRemate: f.nombre || ('Feria ' + f.localidad), fecha: f.fecha, hora: f.hora, cabezas: Number(f.cabezas) || 0, latitud: f.lat, longitud: f.lon, businessUnit: 'feria', youtubeUrl: f.youtube || '' };
20113    const local = { title: payload.nombreRemate, consignataria: f.consignataria, provincia: f.provincia, localidad: f.localidad, cabezas: payload.cabezas, youtubeUrl: f.youtube || '', fecha: iso };
20114    let token = '';
20115    try { token = await (await this.ensureFirebase()).auth().currentUser?.getIdToken(); } catch {}
20116    if (!token) { this.setState({ feriaPhase: 'error', feriaError: 'Iniciá sesión para generar la transmisión.' }); return; }
20117    try {
20118      const r = await fetch('/api/ferias' + (this.state.feriaEditingId ? '/' + encodeURIComponent(this.state.feriaEditingId) : ''), { method: this.state.feriaEditingId ? 'PUT' : 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify(payload) });
20119      const j = await r.json().catch(() => null);
20120      if (!r.ok || j?.ok === false) throw Object.assign(new Error(j?.message || 'No se pudo crear la feria.'), {status:r.status});
20121      if (this.state.feriaEditingId) { this.setState({feriaEditingId:null,feriaPhase:'idle'}); this.go('profile'); this.refreshProfileData(true); this.showToast('Feria actualizada.'); return; }
20122      const created = j?.data || { ...local, youtubeUrl: j?.youtubeUrl || local.youtubeUrl };
20123      await this.waitFeriaYoutube(created);
20124    } catch (e) {
20125      const conflict = e.status === 409;
20126      const message = conflict ? 'No se creó la feria ni la transmisión: ya existe una feria en esa fecha y localidad. Elegí otra fecha o localidad.' : String(e.message || e);
20127      this.setState({ feriaPhase: conflict ? 'idle' : 'error', feriaError: message, ...(conflict ? {feriaResult:null,feriaFormMessage:{ok:false,text:message}} : {}) });
20128    }
20129  }
20130  feriaYoutubeReady(auction) {
20131    const status = String(auction?.youtubeStatus || '').toLowerCase();
20132    if (!['scheduled', 'created', 'ready', 'live', 'complete'].includes(status)) return false;
20133    try { const u = new URL(auction.youtubeUrl); return u.protocol === 'https:' && ['youtube.com', 'www.youtube.com', 'youtu.be'].includes(u.hostname); } catch { return false; }
20134  }
20135  async waitFeriaYoutube(auction) {
20136    const run = this._feriaWaitRun = (this._feriaWaitRun || 0) + 1;
20137    this.setState({ feriaResult: auction, feriaPhase: 'creating', feriaError: '' });
20138    try {
20139      for (let attempt = 0; attempt < 25; attempt++) {
20140        if (this._feriaWaitRun !== run) return;
20141        this.setState({ feriaResult: auction });
20142        if (String(auction.youtubeStatus).toLowerCase() === 'failed') {
20143          this.setState({ feriaPhase: 'error', feriaError: auction.youtubeError || 'La feria está guardada, pero YouTube no pudo crear la transmisión.' });
20144          return;
20145        }
20146        if (this.feriaYoutubeReady(auction)) { this.finishFeria(auction); return; }
20147        if (!auction.id) throw new Error('No recibimos el identificador de la feria. Verificá tus ferias antes de volver a crearla.');
20148        if (attempt === 24) break;
20149        await new Promise(resolve => setTimeout(resolve, attempt < 3 ? 2000 : 5000));
20150        if (this._feriaWaitRun !== run) return;
20151        const response = await this.apiFetch('/api/ferias/' + encodeURIComponent(auction.id));
20152        if (this._feriaWaitRun !== run) return;
20153        auction = { ...auction, ...(response.data || response) };
20154      }
20155      this.setState({ feriaPhase: 'waiting', feriaError: 'La feria está guardada. YouTube todavía no confirmó la transmisión. Podés consultar nuevamente sin crear otra feria.' });
20156    } catch (error) {
20157      if (this._feriaWaitRun === run) this.setState({ feriaPhase: 'waiting', feriaError: 'La feria está guardada. No pudimos confirmar el estado: ' + error.message });
20158    }
20159  }
20160  async retryFeriaYoutube() {
20161    const auction = this.state.feriaResult;
20162    if (!auction?.id || this.state.feriaDeleting || ['connecting', 'creating'].includes(this.state.feriaPhase)) return;
20163    const retry = this.state.feriaPhase === 'error' && auction.youtubeStatus === 'failed';
20164    this.setState({ feriaPhase: 'creating', feriaError: '' });
20165    try {
20166      const response = await this.apiFetch('/api/ferias/' + encodeURIComponent(auction.id) + (retry ? '/youtube-link' : ''), retry ? { method: 'POST', body: JSON.stringify({ force: false }) } : {});
20167      await this.waitFeriaYoutube({ ...auction, ...(response.data || response) });
20168    } catch (error) { this.setState({ feriaPhase: retry ? 'error' : 'waiting', feriaError: error.message }); }
20169  }
20170  async deleteSavedFeria(auction) {
20171    if (!auction?.id || this.state.feriaDeleting || ['connecting', 'creating'].includes(this.state.feriaPhase)) return;
20172    if (!window.confirm('¿Eliminar la feria "' + (auction.nombreRemate || auction.title || auction.localidad || auction.id) + '"? También se eliminará su transmisión de YouTube si existe. Esta acción no se puede deshacer.')) return;
20173    this.setState({ feriaDeleting: true });
20174    try {
20175      await this.apiFetch('/api/ferias/' + encodeURIComponent(auction.id), { method: 'DELETE' });
20176      this.setState({ feriaCreated: (this.state.feriaCreated || []).filter(f => f.id !== auction.id), profileFerias: (this.state.profileFerias || []).filter(f => f.id !== auction.id), homeCalendarCache: {} });
20177      if (this.state.feriaResult?.id === auction.id) this.resetFeriaCreator();
20178      this.fetchMonthRemates();
20179      this.showToast('Feria eliminada');
20180    } catch (error) {
20181      this.showToast(error.message || 'No se pudo eliminar la feria.');
20182      if (this.state.feriaResult?.id === auction.id) this.setState({ feriaError: error.message });
20183    } finally { this.setState({ feriaDeleting: false }); }
20184  }
20185  shareFeriaFlyer() {
20186    const auction = this.state.feriaResult;
20187    if (!auction) return;
20188    const fullLogo = this.consFullHeroLogo(auction.consignataria);
20189    const logo = fullLogo || this.consLogo(auction.consignataria) || this.state.feriaForm.consignatariaLogo;
20190    window.RosganFeriaFlyer.open(auction, {color:this.consBrandColor(auction.consignataria),logo,mono:!!fullLogo});
20191  }
20192  finishFeria(created) {
20193    if (!this.feriaYoutubeReady(created)) return;
20194    this.setState({ feriaPhase: 'done', feriaResult: created, feriaCreated: [created, ...this.state.feriaCreated.filter(f => f.id !== created.id)] });
20195    this.setState({ homeCalendarCache:{} });
20196    this.fetchMonthRemates();
20197  }
20198  resetFeriaCreator() {
20199    this._feriaWaitRun = (this._feriaWaitRun || 0) + 1;
20200    const assigned = this.feriaAssignedConsignataria();
20201    const assignedName = assigned ?(this.consName(assigned) || assigned.name) : '';
20202    this.setState({ feriaPhase: 'idle', feriaResult: null, feriaError: '', feriaTitleAuto: true, feriaFavoriteId:'', feriaManualOpen:false, feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '',
20203      feriaEditingId: null, feriaForm: { consignataria: assignedName, consignatariaLogo: assigned?.logoUrl || this.consLogo(assignedName), provincia: '', localidad: '', nombre: '', fecha: '', hora: '', cabezas: '', youtube: '', lat: '', lon: '' } });
20204  }
20205  copyText(t) { try { navigator.clipboard.writeText(t); this.showToast('Enlace copiado'); } catch {} }
20206  accentFromName(name) { let h = 0; for (const ch of (name || 'Rosgan')) h = (h * 31 + ch.charCodeAt(0)) % 360; return 'hsl(' + h + ', 55%, 42%)'; }
20207  extractLogoColor(name, url) {
20208    if (!name || !url) return;
20209    const key = this.consKey(name);
20210    this._colors = this._colors || {};
20211    if (this._colors[key]) return;
20212    this._colors[key] = 'pending';
20213    const img = new Image(); img.crossOrigin = 'anonymous';
20214    img.onload = () => {
20215      try {
20216        const c = document.createElement('canvas'); const S = 24; c.width = S; c.height = S;
20217        const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0, S, S);
20218        const d = ctx.getImageData(0, 0, S, S).data;
20219        let r = 0, g = 0, b = 0, n = 0;
20220        for (let i = 0; i < d.length; i += 4) {
20221          const R = d[i], G = d[i + 1], B = d[i + 2], A = d[i + 3];
20222          if (A < 128) continue;
20223          const mx = Math.max(R, G, B), mn = Math.min(R, G, B);
20224          if (mx > 235 && mn > 220) continue; // skip near-white
20225          if (mx < 30) continue; // skip near-black
20226          if (mx - mn < 18 && mx > 150) continue; // skip light grays
20227          r += R; g += G; b += B; n++;
20228        }
20229        let col;
20230        if (n > 4) { col = 'rgb(' + Math.round(r / n) + ',' + Math.round(g / n) + ',' + Math.round(b / n) + ')'; }
20231        else col = this.accentFromName(name);
20232        this._colors[key] = col;
20233        this.setState({ colorTick: (this.state.colorTick || 0) + 1 });
20234      } catch { this._colors[key] = this.accentFromName(name); this.setState({ colorTick: (this.state.colorTick || 0) + 1 }); }
20235    };
20236    img.onerror = () => { this._colors[key] = this.accentFromName(name); this.setState({ colorTick: (this.state.colorTick || 0) + 1 }); };
20237    img.src = url;
20238  }
20239  consColor(name) { if (!name) return '#0a3e78'; const brand = this.consLocalBrand(name); if (brand) return brand.rgb; const c = (this._colors || {})[this.consKey(name)]; return (c && c !== 'pending') ?c : this.accentFromName(name); }
20240  consBrandColor(name) {
20241    if (!this.consKey(name)) return '#0a3e78';
20242    const brand = this.consLocalBrand(name);
20243    if (brand) return brand.color;
20244    const key = this.consKey(name);
20245    const accents = {
20246      rosgan: '#002e5f',
20247      alzagaunzueyciasa: '#32557B', alzagaunzue: '#32557B',
20248      ildarrazhnossa: '#D40612', ildarraz: '#D40612',
20249      aguirrevazquezsa: '#5967D8', aguirrevazquez: '#5967D8',
20250      colomboycolombosa: '#D71945', colomboycolombo: '#D71945',
20251      coopguillermolehmann: '#008D46', guillermolehmann: '#008D46', coopglehmann: '#008D46', cooplehmann: '#008D46',
20252      edgarepastoreyciasrl: '#1F325C', pastore: '#1F325C',
20253      etchevehereruralsrl: '#1F4AA1', etchevehere: '#1F4AA1',
20254      ferialvarezsrl: '#009D92', ferialvarez: '#009D92',
20255      ganaderosdecerescoopltda: '#36A9E1', ganaderosdeceres: '#36A9E1',
20256      coopldtaunionagricoladeavellaneda: '#70292D', coopunionagricoladeavellaneda: '#70292D', unionagricoladeavellaneda: '#70292D', coopdeavellaneda: '#70292D',
20257      rigran: '#F36C18',
20258    };
20259    const exact = accents[key];
20260    if (exact) return exact;
20261    const partial = Object.entries(accents).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)));
20262    return partial?.[1] || this.consColor(name);
20263  }
20264  consWhiteLogo(name) {
20265    if (!this.consKey(name)) return '';
20266    const brand = this.consLocalBrand(name);
20267    if (brand) return brand.whiteLogo;
20268    const key = this.consKey(name);
20269    if (key === 'rosgan') return 'assets/brand/rosgan-iso-white.png';
20270    const logos = {
20271      alzagaunzueyciasa: 'alzaga-unzue-y-cia-s-a', alzagaunzue: 'alzaga-unzue-y-cia-s-a',
20272      ildarrazhnossa: 'ildarraz-hnos-s-a', ildarraz: 'ildarraz-hnos-s-a',
20273      aguirrevazquezsa: 'aguirre-vazquez-s-a', aguirrevazquez: 'aguirre-vazquez-s-a',
20274      colomboycolombosa: 'colombo-y-colombo-s-a', colomboycolombo: 'colombo-y-colombo-s-a',
20275      coopguillermolehmann: 'coop-guillermo-lehmann', guillermolehmann: 'coop-guillermo-lehmann', coopglehmann: 'coop-guillermo-lehmann', cooplehmann: 'coop-guillermo-lehmann',
20276      edgarepastoreyciasrl: 'edgar-e-pastore-y-cia-s-r-l', pastore: 'edgar-e-pastore-y-cia-s-r-l',
20277      etchevehereruralsrl: 'etchevehere-rural-s-r-l', etchevehere: 'etchevehere-rural-s-r-l',
20278      ferialvarezsrl: 'ferialvarez-s-r-l', ferialvarez: 'ferialvarez-s-r-l',
20279      ganaderosdecerescoopltda: 'ganaderos-de-ceres-coop-ltda', ganaderosdeceres: 'ganaderos-de-ceres-coop-ltda',
20280      coopldtaunionagricoladeavellaneda: 'coop-union-agricola-avellaneda', coopunionagricoladeavellaneda: 'coop-union-agricola-avellaneda', unionagricoladeavellaneda: 'coop-union-agricola-avellaneda', coopdeavellaneda: 'coop-union-agricola-avellaneda',
20281      rigran: 'rigran',
20282    };
20283    const slug = logos[key] || Object.entries(logos).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1];
20284    return slug ?'assets/consignatarias-white/' + slug + (slug.endsWith('.svg') ?'' : '.png') : '';
20285  }
20286  shade(rgb, f) {
20287    const m = String(rgb).match(/(\d+),\s*(\d+),\s*(\d+)/);
20288    if (!m) return rgb;
20289    const adj = (v) => Math.max(0, Math.min(255, Math.round(f > 0 ?v + (255 - v) * f : v * (1 + f))));
20290    return 'rgb(' + adj(+m[1]) + ',' + adj(+m[2]) + ',' + adj(+m[3]) + ')';
20291  }
20292  REMATE_TYPES = { cabanas: ['Cabañas', '#53bec5'], especiales: ['Especiales', '#ea8d00'], faena: ['Faena', '#ab2f38'], habitual: ['Habitual', '#8a9000'], feria: ['Feria', '#7a8595'], libre: ['Libre', '#00973a'], aniversario: ['Aniversario', '#b99632'], rosgan: ['Rosgan', '#002e5f'] };
20293  remateType(item) {
20294    if (item?.__feriaAuction) return this.REMATE_TYPES.feria;
20295    const s = (item.nombre_tipo || item.nombre_remate || item.tipo_remate || '').toLowerCase();
20296    for (const k of ['cabañas', 'cabana', 'especial', 'faena', 'habitual', 'feria', 'libre', 'aniversario']) {
20297      if (s.includes(k)) { const key = k.startsWith('caban') ?'cabanas' : k.includes('especial') ?'especiales' : k; return this.REMATE_TYPES[key] || this.REMATE_TYPES.rosgan; }
20298    }
20299    return this.REMATE_TYPES.rosgan;
20300  }
20301  latestFinishedRemates(historical, current) {
20302    const active = new Set(this.rosganUpcomingRemates(current).map((item) => this.rosganArchiveIdentity(item)));
20303    return this.rosganArchiveRemates(historical, current, null)
20304      .filter((item) => !active.has(this.rosganArchiveIdentity(item)) && (item.__rosganClosedArchive || (this.remateDate(item) && this.remateDate(item) < new Date())))
20305      .sort((a, b) => this.remateDate(b) - this.remateDate(a)).slice(0, 5);
20306  }
20307  unifiedArchiveRemates(historical, current, cabanas, year) {
20308    const selectedYear = Number(year);
20309    const rosgan = this.rosganArchiveRemates(historical, current, year);
20310    const cabana = (cabanas || [])
20311      .filter((item) => this.cabanaMode(item) === 'recent' && !this.isExcludedCabana(item))
20312      .filter((item) => !selectedYear || this.cabanaAuctionStartAt(item)?.getFullYear() === selectedYear)
20313      .map((item) => ({ ...item, __cabanaAuction: true }));
20314    const seen = new Set();
20315    return [...rosgan, ...cabana].filter((item) => {
20316      const date = item.__cabanaAuction ?this.cabanaAuctionStartAt(item) : this.remateDate(item);
20317      const title = item.nombre_remate || item.titulo || item.title || '';
20318      const key = this.consKey(title) + ':' + (date?.getFullYear() || '');
20319      if (!key || seen.has(key)) return false;
20320      seen.add(key);
20321      return true;
20322    });
20323  }
20324  unifiedLatestFinishedRemates(historical, current, cabanas) {
20325    const active = new Set(this.rosganUpcomingRemates(current).map((item) => this.rosganArchiveIdentity(item)));
20326    return this.unifiedArchiveRemates(historical, current, cabanas, null)
20327      .filter((item) => item.__cabanaAuction || (!active.has(this.rosganArchiveIdentity(item)) && (item.__rosganClosedArchive || (this.remateDate(item) && this.remateDate(item) < new Date()))))
20328      .sort((a, b) => {
20329        const bDate = b.__cabanaAuction ?this.cabanaAuctionStartAt(b) : this.remateDate(b);
20330        const aDate = a.__cabanaAuction ?this.cabanaAuctionStartAt(a) : this.remateDate(a);
20331        return (bDate || 0) - (aDate || 0);
20332      }).slice(0, 5);
20333  }
20334  async fetchLatestFinishedRemates() {
20335    if (this.state.latestFinishedLoading || Array.isArray(this.state.latestFinished)) return;
20336    this.setState({ latestFinishedLoading: true });
20337    const items = [], seen = new Set();
20338    const years = [new Date().getFullYear()];
20339    try {
20340      for (let yi = 0; yi < years.length; yi++) {
20341        for (let page = 1; ; page++) {
20342          const data = await this.unwrap('/api/remateshistorico?scope=all&year=' + years[yi] + '&page=' + page + '&pageSize=20');
20343          if (yi === 0 && page === 1) years.push(...(data?.years || []).map(Number).filter((year) => year < years[0]).sort((a, b) => b - a));
20344          for (const item of (data?.items || [])) {
20345            const key = this.rosganArchiveIdentity(item);
20346            if (!seen.has(key)) { seen.add(key); items.push(item); }
20347          }
20348          if (this.latestFinishedRemates(items, []).length >= 5 || page >= (data?.totalPages || 1)) break;
20349        }
20350        if (this.latestFinishedRemates(items, []).length >= 5) break;
20351      }
20352      this.setState({ latestFinished: items, latestFinishedLoading: false, latestFinishedError: '' });
20353      this.loadRemateListCatalogs(items);
20354    } catch { this.setState({ latestFinishedLoading: false, latestFinishedError: 'No se pudieron cargar los últimos remates.' }); }
20355  }
20356  async fetchRemates(year) {
20357    this.setState({ rematesLoading: true });
20358    try {
20359      const data = await this.unwrap('/api/remateshistorico?scope=all&year=' + year + '&page=1&pageSize=120');
20360      const items = Array.isArray(data) ?data : (data?.items || data?.remates || []);
20361      this.loadRemateListCatalogs(items, 'historical');
20362      this.setState({ remates: items, remateYears: (data?.years || []).map(String), rematesLoading: false });
20363    } catch { this.setState({ remates: [], rematesLoading: false }); }
20364  }
20365  cabanaCatalogPdfUrl(item) {
20366    const value = window.resolveCabanaMediaUrl(item?.catalog?.url || '');
20367    if (!value) return '';
20368    try {
20369      const url = new URL(value, window.location.origin);
20370      return ['https:', 'http:'].includes(url.protocol) ? url.href : '';
20371    } catch { return ''; }
20372  }
20373  cabanaBreedOf(item) {
20374    const text = String(item?.raza || item?.nombre_remate || item?.title || item?.titulo || '').toLowerCase();
20375    const breeds = [
20376      ['Angus Colorado', /angus\s+colorad/], ['Aberdeen Angus', /aberdeen|angus\s+negro|\bangus\b/],
20377      ['Polled Hereford', /polled\s+hereford/], ['Hereford', /hereford/], ['Brangus', /brangus/],
20378      ['Braford', /braford/], ['Limousin', /limousin/], ['Charolais', /charolais/],
20379    ];
20380    return breeds.find(([, pattern]) => pattern.test(text))?.[0] || '';
20381  }
20382  cabanaConsignatariaName(item, consignatarias = []) {
20383    const ref = item?.consignatariaId || item?.consignataria || item?.nombre_consignatario || item?.nombre_cuenta || '';
20384    const record = (consignatarias || []).find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.value, candidate?.name, candidate?.nombre, candidate?.label]
20385      .some((value) => this.consKey(value) && this.consKey(value) === this.consKey(ref)));
20386    const explicit = item?.consignatariaNombre || item?.nombreConsignataria || item?.consignataria?.nombre || item?.consignataria?.name || '';
20387    return explicit || (record ?this.consName(record) : '') || (String(ref).includes('-') ?this.humanizeSlug(ref) : String(ref)) || 'Rosgan';
20388  }
20389  cabanaColor(item) {
20390    const primary = Array.isArray(item?.cabanasPrincipales) && item.cabanasPrincipales.length
20391       ? item.cabanasPrincipales[0]
20392      : (Array.isArray(item?.cabins) ?item.cabins.find((cabana) => String(cabana?.type || '').toLowerCase() === 'principal') : null);
20393    const loadedColor = String(primary?.brandColor || item?.brandColor || '');
20394    if (/^#[0-9a-f]{6}$/i.test(loadedColor)) return loadedColor;
20395    const colors = { 'Aberdeen Angus': '#26323d', 'Angus Colorado': '#8d3327', Brangus: '#8b552c', 'Polled Hereford': '#a43831', Hereford: '#a43831', Braford: '#b06a2e', Limousin: '#b28a31', Charolais: '#737f8d' };
20396    return colors[this.cabanaBreedOf(item)] || '#0a5ca8';
20397  }
20398  cabanaBrandDark(item) {
20399    const color = this.cabanaColor(item);
20400    const match = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
20401    if (!match) return '#002e5f';
20402    const darken = (value) => Math.max(8, Math.round(parseInt(value, 16) * 0.5)).toString(16).padStart(2, '0');
20403    return '#' + darken(match[1]) + darken(match[2]) + darken(match[3]);
20404  }
20405  async ensureCabanaBrand(item) {
20406    item = this.normalizeCabanaAuctionRecord(item);
20407    const groups = ['cabanasPrincipales', 'cabanasInvitadas'];
20408    let changed = false;
20409    const next = { ...item };
20410    for (const group of groups) {
20411      next[group] = await Promise.all((item?.[group] || []).map(async (cabana) => {
20412        if (!cabana?.logoDataUrl || cabana?._whiteLogoVersion === 2) return cabana;
20413        try {
20414          const generated = await this.processCabanaLogoDataUrl(cabana.logoDataUrl);
20415          changed = true;
20416          return { ...cabana, ...generated, _whiteLogoVersion: 2 };
20417        } catch { return cabana; }
20418      }));
20419    }
20420    if (!changed) return item;
20421    const id = this.cabanaRemateId(item);
20422    this.setState((state) => ({
20423      currentCabana: this.cabanaRemateId(state.currentCabana) === id ?next : state.currentCabana,
20424      cabanas: Array.isArray(state.cabanas) ?state.cabanas.map((remate) => this.cabanaRemateId(remate) === id ?next : remate) : state.cabanas,
20425    }));
20426    return next;
20427  }
20428  normalizeCabanaAuctionRecord(item) {
20429    if (!item || typeof item !== 'object') return item;
20430    const normalized = { ...item };
20431    const cabins = Array.isArray(normalized.cabins) ?normalized.cabins : [];
20432    const hasPrincipals = Array.isArray(normalized.cabanasPrincipales) && normalized.cabanasPrincipales.length;
20433    const hasGuests = Array.isArray(normalized.cabanasInvitadas) && normalized.cabanasInvitadas.length;
20434    if ((!hasPrincipals || !hasGuests) && cabins.length) {
20435      const mapCabin = (cabana) => ({
20436        id: cabana?.id || cabana?.cabanaId || null,
20437        cabanaId: cabana?.cabanaId || cabana?.id || null,
20438        nombre: cabana?.nombre || cabana?.name || '',
20439        name: cabana?.name || cabana?.nombre || '',
20440        logoName: cabana?.logoName || '',
20441        logoDataUrl: cabana?.logoDataUrl || cabana?.logoUrl || '',
20442        logoUrl: cabana?.logoUrl || cabana?.logoDataUrl || '',
20443        logoWhiteDataUrl: cabana?.logoWhiteDataUrl || '',
20444        brandColor: cabana?.brandColor || '',
20445        displayOrder: cabana?.displayOrder || 0,
20446      });
20447      const ordered = cabins.slice().sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0));
20448      if (!hasPrincipals) normalized.cabanasPrincipales = ordered.filter((cabana) => String(cabana?.type || '').toLowerCase() === 'principal').map(mapCabin);
20449      if (!hasGuests) normalized.cabanasInvitadas = ordered.filter((cabana) => String(cabana?.type || '').toLowerCase() !== 'principal').map(mapCabin);
20450    }
20451    return normalized;
20452  }
20453  cabanaAuctionStartAt(item) {
20454    if (!item || typeof item !== 'object') return null;
20455    const rawDate = item.schedule?.fecha || item.fecha_remate || item.fecha || item.fecha_hora_remate || item.fechaHoraInicio || item.startsAt || item.startAt || '';
20456    const rawTime = String(item.schedule?.hora || item.hora_remate || item.hora || '').trim();
20457    const parts = this.remateLocalDateTimeParts(rawDate);
20458    const time = rawTime || parts.time || '00:00';
20459    const iso = this.rematePayloadDateTime(parts.date, time);
20460    const start = iso ? new Date(iso) : null;
20461    return start && !Number.isNaN(start.getTime()) ? start : null;
20462  }
20463  cabanaMode(item) {
20464    const start = this.cabanaAuctionStartAt(item);
20465    if (!start) return 'upcoming';
20466    const now = new Date();
20467    if (now >= start && now <= new Date(start.getTime() + 5 * 3600e3)) return 'live';
20468    return now < start ?'upcoming' : 'recent';
20469  }
20470  cabanaListingGroup(item) {
20471    if (this.cabanaMode(item) === 'recent') return 'recent';
20472    const loadingClosed = item?.load?.lotes === false || item?.cargaLotes === false || String(item?.cargaLotes || '').toLowerCase() === 'cerrada';
20473    return loadingClosed ?'upcoming' : 'active';
20474  }
20475  compareCabanaListing(a, b) {
20476    const rank = {active:0,upcoming:1,recent:2};
20477    const ga = this.cabanaListingGroup(a), gb = this.cabanaListingGroup(b);
20478    const group = rank[ga] - rank[gb];
20479    if (group) return group;
20480    const da = this.cabanaAuctionStartAt(a)?.getTime(), db = this.cabanaAuctionStartAt(b)?.getTime();
20481    if (da == null || db == null) return da == null && db == null ? String(this.cabanaRemateId(a)).localeCompare(String(this.cabanaRemateId(b))) : da == null ? 1 : -1;
20482    return (ga === 'recent' ? db - da : da - db) || String(this.cabanaRemateId(a)).localeCompare(String(this.cabanaRemateId(b)));
20483  }
20484  cabanaMedia(item) {
20485    return window.resolveCabanaMediaUrl(item?.youtube?.thumbnailUrl || item?.thumbnailUrl || item?.thumbnail || item?.
20485imagen);
20486  }
20487  cabanaWeatherLocation(item = this.state.currentCabana) {
20488    const location = item?.location || {};
20489    const locality = String(item?.localidad || location.localidad || location.locality || '').trim();
20490    const province = String(item?.provincia || location.provinciaNombre || location.provincia || location.province || '').trim();
20491    let label = [locality, province].filter(Boolean).join(', ');
20492    if (!label) label = String(item?.ubicacion_remate || location.lugar || item?.lugar || '').trim();
20493    label = label.replace(/\bSociedad Rural de\b/ig, '').replace(/\s+/g, ' ').trim();
20494    const known = label.match(/trenque\s+lauquen/i);
20495    if (known) label = [known[0].replace(/\s+/g, ' '), province].filter(Boolean).join(', ');
20496    return label || '';
20497  }
20498  cabanaWeatherCode(code) {
20499    const n = Number(code);
20500    if ([0].includes(n)) return { label: 'Despejado', icon: 'sun' };
20501    if ([1, 2].includes(n)) return { label: 'Algo nublado', icon: 'cloud-sun' };
20502    if ([3].includes(n)) return { label: 'Nublado', icon: 'cloud' };
20503    if ([45, 48].includes(n)) return { label: 'Niebla', icon: 'cloud-fog' };
20504    if ((n >= 51 && n <= 67) || (n >= 80 && n <= 82)) return { label: 'Lluvia', icon: 'cloud-rain' };
20505    if (n >= 71 && n <= 77) return { label: 'Nieve', icon: 'cloud-snow' };
20506    if (n >= 95) return { label: 'Tormenta', icon: 'cloud-lightning' };
20507    return { label: 'Clima', icon: 'cloud-sun' };
20508  }
20509  async fetchCabanaTvWeather(item = this.state.currentCabana, options = {}) {
20510    const locationLabel = this.cabanaWeatherLocation(item);
20511    if (!locationLabel) {
20512      if (this.state.cabanaTvWeather || this.state.cabanaTvWeatherLocation) this.setState({ cabanaTvWeather: null, cabanaTvWeatherLocation: '', cabanaTvWeatherError: '' });
20513      return null;
20514    }
20515    const now = Date.now();
20516    if (!options.force && this._cabanaTvWeatherLastAt && this._cabanaTvWeatherLocation === locationLabel && now - this._cabanaTvWeatherLastAt < 10 * 60 * 1000) return this.state.cabanaTvWeather;
20517    if (this._cabanaTvWeatherLoading) return this.state.cabanaTvWeather;
20518    this._cabanaTvWeatherLoading = true;
20519    this._cabanaTvWeatherLocation = locationLabel;
20520    this.setState({ cabanaTvWeatherLoading: true, cabanaTvWeatherLocation: locationLabel });
20521    try {
20522      const geoUrl = 'https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(locationLabel) + '&count=1&language=es&format=json';
20523      const geoResponse = await fetch(geoUrl, { cache: 'no-store' });
20524      if (!geoResponse.ok) throw new Error('Geocoding HTTP ' + geoResponse.status);
20525      const geoData = await geoResponse.json();
20526      const place = Array.isArray(geoData?.results) ?geoData.results[0] : null;
20527      if (!place || place.latitude == null || place.longitude == null) throw new Error('Sin ubicacion de clima');
20528      const weatherUrl = 'https://api.open-meteo.com/v1/forecast?latitude=' + encodeURIComponent(place.latitude) + '&longitude=' + encodeURIComponent(place.longitude) + '&current=temperature_2m,weather_code,wind_speed_10m&timezone=America%2FArgentina%2FBuenos_Aires';
20529      const weatherResponse = await fetch(weatherUrl, { cache: 'no-store' });
20530      if (!weatherResponse.ok) throw new Error('Weather HTTP ' + weatherResponse.status);
20531      const weatherData = await weatherResponse.json();
20532      const currentWeather = weatherData?.current || {};
20533      const codeInfo = this.cabanaWeatherCode(currentWeather.weather_code);
20534      const weather = {
20535        location: String(place.name || locationLabel),
20536        temperature: Math.round(Number(currentWeather.temperature_2m || 0)),
20537        unit: weatherData?.current_units?.temperature_2m || '°C',
20538        wind: Math.round(Number(currentWeather.wind_speed_10m || 0)),
20539        windUnit: weatherData?.current_units?.wind_speed_10m || 'km/h',
20540        label: codeInfo.label,
20541        icon: codeInfo.icon,
20542        updatedAt: now,
20543      };
20544      this._cabanaTvWeatherLastAt = now;
20545      this.setState({ cabanaTvWeather: weather, cabanaTvWeatherLoading: false, cabanaTvWeatherError: '' });
20546      return weather;
20547    } catch (error) {
20548      this.setState({ cabanaTvWeatherLoading: false, cabanaTvWeatherError: error?.message || 'No se pudo cargar clima' });
20549      return null;
20550    } finally {
20551      this._cabanaTvWeatherLoading = false;
20552    }
20553  }
20554  isExcludedCabana(item) {
20555    return this.isExcludedRemate(item);
20556  }
20557  canEditCabanaRemate(item) {
20558    if (this.canManageUsers(this.state.apiUser)) return true;
20559    const currentUid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || '');
20560    const currentEmail = String(this.state.apiUser?.email || this.state.user?.email || '').trim().toLowerCase();
20561    const ownerUid = String(item?.creatorUid || item?.createdByUid || item?.ownerUid || item?.userUid || '');
20562    const ownerEmail = String(item?.creatorEmail || item?.createdByEmail || item?.ownerEmail || item?.userEmail || '').trim().toLowerCase();
20563    if ((ownerUid && currentUid && ownerUid === currentUid) || (ownerEmail && currentEmail && ownerEmail === currentEmail)) return true;
20564    return false;
20565  }
20566  canEditCabanaLot(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) {
20567    if (this.state.publicAdvanceMode || !remate || !lot) return false;
20568    return this.apiRoleIds(this.state.apiUser).some((role) => ['admin', 'sysadmin', 'consignatario', 'cabanero'].includes(role));
20569  }
20570  canUploadCabanaLotMedia(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) {
20571    if (!this.canEditCabanaLot(remate,lot)) return false;
20572    return this.canExportCabanaCatalog(remate, this.state.apiUser) || this.apiRoleIds(this.state.apiUser).includes('cabanero');
20573  }
20574  canUploadCabanaFinal(remate = this.state.currentCabana) {
20575    return !this.state.publicAdvanceMode && window.CabanaPrebid.canUploadFinal(this.apiRoleIds(this.state.apiUser),remate);
20576  }
20577  async chooseCabanaVideoOptions(remate, primaryLotId = '') {
20578    const uid=this.state.user?.uid;
20579    const auctionId=this.cabanaRemateId(remate);
20580    const response=await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(auctionId)+'/lots');
20581    if (this.state.user?.uid !== uid) return null;
20582    const lots=this.normalizeCabanaAuctionLots(this.listFrom(response,['data','items','lots','lotes','groups']),remate);
20583    const unique=[...new Map(lots.map(lot=>[this.cabanaLotId(lot),lot])).values()];
20584    const options=[{value:'raw',label:'Original (raw)'}];
20585    if(this.canUploadCabanaFinal(remate)) options.push({value:'final',label:'Video final'});
20586    const result=await window.CabanaPrebid.editDialog({title:'Cargar video',description:'Elegí el tipo de video y los lotes del remate que lo compartirán. El lote actual siempre queda incluido.',submitLabel:'Continuar carga',fields:[{key:'videoType',label:'Tipo de video',value:options.at(-1).value,options}],choices:unique.map(lot=>({value:this.cabanaLotId(lot),label:'Lote '+this.cabanaLotNumber(lot)+' · '+(lot.categoryName || ''),checked:this.cabanaLotId(lot)===primaryLotId,disabled:this.cabanaLotId(lot)===primaryLotId}))});
20587    if (this.state.user?.uid !== uid) return null;
20588    if(result?.videoType === 'final' && !this.canUploadCabanaFinal(remate)) throw new Error('No tenés permiso para cargar videos finales.');
20589    return result;
20590  }
20591  async openCabanaAuditor(item) {
20592    const remate = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
20593    if (!this.canAuditCabana(remate)) return this.denyCabanaPrivatePage(remate);
20594    const same = this.cabanaRemateId(remate) === this.cabanaRemateId(this.state.currentCabana);
20595    this.syncCabanaLocation(remate, 'auditor');
20596    this.setState({route:'cabanaAuditor',currentCabana:remate,currentCabanaLot:null,cabanaLots:same ? this.state.cabanaLots : null,cabanaLotsLoading:true,cabanaPublicDeepLoading:false,cabanaBrandPreparing:false,cabanaAuditorQuery:''});
20597    window.scrollTo(0,0);
20598    await this.fetchCabanaLots(remate);
20599  }
20600  canAuditCabana(remate = this.state.currentCabana) {
20601    return !this.state.publicAdvanceMode && this.canExportCabanaCatalog(remate, this.state.apiUser);
20602  }
20603  async saveCabanaAuditorPricing(remate, lot, event) {
20604    event?.preventDefault?.();
20605    if (!this.canAuditCabana(remate) || !this.canEditCabanaLot(remate, lot)) return;
20606    const key = this.cabanaRemateId(remate) + ':' + this.cabanaPrebidCert(lot);
20607    if (this.state.cabanaAuditorSaving?.[key]) return;
20608    const draft = this._cabanaAuditorDrafts?.[key];
20609    if (!draft) return;
20610    const base = String(draft.base).trim(), increment = String(draft.increment).trim();
20611    if ((base !== '' && (!Number.isFinite(Number(base)) || Number(base) < 0)) || (increment !== '' && (!Number.isFinite(Number(increment)) || Number(increment) <= 0))) { this.showToast('Ingresá una base mayor o igual a cero y un incremento mayor a cero.'); return; }
20612    this.setState(state => ({ cabanaAuditorSaving: { ...state.cabanaAuditorSaving, [key]: true } }));
20613    try {
20614      await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(this.cabanaRemateId(remate)) + '/lots/' + encodeURIComponent(this.cabanaPrebidCert(lot)), { method: 'PATCH', body: JSON.stringify({ updatePrebidPricing: true, prebidBaseOverride: base === '' ? null : Number(base), prebidIncrementOverride: increment === '' ? null : Number(increment) }) });
20615      delete this._cabanaAuditorDrafts[key];
20616      await this.fetchCabanaLots(remate);
20617      this.showToast('Base e incremento guardados.');
20618    } catch (error) { this.showToast(error?.data?.detail || error.message || 'No se pudo guardar.'); }
20619    finally { this.setState(state => ({ cabanaAuditorSaving: { ...state.cabanaAuditorSaving, [key]: false } })); }
20620  }
20621  async editCabanaPrebidPricing() {
20622    const lot=this.state.currentCabanaLot, remate=this.state.currentCabana;
20623    if(!this.canEditCabanaLot(remate,lot) || this._cabanaPricingBusy) return;
20624    this._cabanaPricingBusy=true;
20625    try {
20626      const {inherited,base,increment}=window.CabanaPrebid.pricing(lot,remate);
20627      const fmt=v=>v==null?'Sin configurar':this.fmtARS(v);
20628      const result=await window.CabanaPrebid.editDialog({title:'Base e incremento del lote',description:'Categoría: base '+fmt(inherited.baseAmount)+' · incremento '+fmt(inherited.incrementAmount)+'. Valores efectivos: '+fmt(base)+' / '+fmt(increment)+'. Dejá un campo vacío para heredar ese valor.',resetLabel:'Volver a usar configuración de la categoría',fields:[{key:'base',label:'Base personalizada',value:lot.prebidBaseOverride,min:0},{key:'increment',label:'Incremento personalizado',value:lot.prebidIncrementOverride,min:0.01}]});
20629      if(!result) return;
20630      const payload={updatePrebidPricing:true,prebidBaseOverride:result.reset || result.base === ''?null:Number(result.base),prebidIncrementOverride:result.reset || result.increment === ''?null:Number(result.increment)};
20631      await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(this.cabanaRemateId(remate))+'/lots/'+encodeURIComponent(this.cabanaPrebidCert(lot)),{method:'PATCH',body:JSON.stringify(payload)});
20632      const lots=await this.fetchCabanaLots(remate);
20633      const updated=lots.find(l=>this.cabanaPrebidCert(l)===this.cabanaPrebidCert(lot) && l.itemId===lot.itemId);
20634      if(updated && this.cabanaPrebidCert(this.state.currentCabanaLot)===this.cabanaPrebidCert(lot)) this.setState({currentCabanaLot:updated});
20635      this.showToast('Configuración de preofertas actualizada.');
20636    } catch(error){this.showToast(error?.data?.detail || error.message);}
20637    finally{this._cabanaPricingBusy=false;}
20638  }
20639  canDeleteCabanaLot(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) {
20640    if (this.state.publicAdvanceMode || !remate || !lot) return false;
20641    return this.apiRoleIds(this.state.apiUser).some((role) => ['admin', 'sysadmin'].includes(role));
20642  }
20643  cabanaLotDrafts(remateId = '') {
20644    try {
20645      const drafts = JSON.parse(localStorage.getItem('rosgan:cabanaLotDrafts') || '[]');
20646      return Array.isArray(drafts) ?drafts.filter((item) => !remateId || String(item?.remateId || '') === String(remateId)) : [];
20647    } catch { return []; }
20648  }
20649  cabanaRemateId(item) { return String(item?.id_remate || item?.sourceId || item?.id || ''); }
20650  cabanaContactData(item = this.state.currentCabana) {
20651    const contact = item?.contacto || item?.contact || {};
20652    const name = String(contact?.name || contact?.nombre || item?.contactName || item?.contactoNombre || 'Equipo Rosgan').trim();
20653    const phone = String(contact?.phone || contact?.telefono || item?.contactPhone || item?.contactoTelefono || '').trim();
20654    const email = String(contact?.email || item?.contactEmail || item?.contactoEmail || '').trim();
20655    const notes = String(contact?.notes || contact?.notas || item?.contactNotes || item?.contactoNotas || '').trim();
20656    const phoneDigits = phone.replace(/\D/g, '');
20657    return { name, phone, email, notes, phoneDigits, phoneHref: phoneDigits ?'tel:+' + phoneDigits : '#', whatsappBase: phoneDigits ?'https://wa.me/' + phoneDigits : '#' };
20658  }
20659  cabanaRemateSchema(item) {
20660    const normalizeSchema = (schema) => (Array.isArray(schema) ?schema : []).map((category) => {
20661      if (Array.isArray(category?.forms)) return category;
20662      const mainFields = Array.isArray(category?.fields) ?category.fields : [];
20663      const subforms = Array.isArray(category?.subforms) ?category.subforms : [];
20664      return {
20665        ...category,
20666        forms: [
20667          { templateId: null, name: 'Datos principales', displayOrder: 0, isActive: true, fields: mainFields },
20668          ...subforms.map((subform) => ({ ...subform, templateId: subform.id || null })),
20669        ].filter((form) => Array.isArray(form.fields) && form.fields.length),
20670      };
20671    });
20672    if (Array.isArray(item?.formsByCategory) && item.formsByCategory.length) return normalizeSchema(item.formsByCategory);
20673    const flatForms = Array.isArray(item?.forms) ?item.forms : [];
20674    if (!flatForms.length) return [];
20675    const grouped = new Map();
20676    flatForms.forEach((form) => {
20677      const categoryId = form?.categoryId || null;
20678      const categoryName = form?.categoryName || form?.categoria || 'Categoría';
20679      const key = String(categoryId || categoryName);
20680      if (!grouped.has(key)) grouped.set(key, { categoryId, categoryName, displayOrder: grouped.size + 1, forms: [] });
20681      grouped.get(key).forms.push(form);
20682    });
20683    return normalizeSchema([...grouped.values()].map((category) => ({
20684      ...category,
20685      forms: category.forms.sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)),
20686    })));
20687  }
20688  clearCabanaLotMedia() {
20689    [...(this.state.cabanaLotImages || []), ...(this.state.cabanaLotVideosUpload || [])].forEac
20689h((media) => { try { URL.revokeObjectURL(media.url); } catch {} });
20690  }
20691  cabanaRemateParticipants(item) {
20692    const normalize = (cabana, type, index) => {
20693      const name = String(cabana?.nombre || cabana?.name || '').trim();
20694      const rawId = cabana?.id || cabana?.cabanaId || null;
20695      return {
20696        id: rawId,
20697        key: type + ':' + String(rawId || name || index),
20698        name: name || 'Cabaña sin nombre',
20699        type,
20700        typeLabel: type === 'principal' ?'Cabaña principal' : 'Cabaña invitada',
20701        logoDataUrl: window.resolveCabanaMediaUrl(cabana?.logoDataUrl || cabana?.logoUrl || (this.consKey(name) === this.consKey('La Paterna') ?'/assets/la-paterna-logo.png' : '')),
20702        logoWhiteDataUrl: window.resolveCabanaMediaUrl(cabana?.logoWhiteDataUrl),
20703        brandColor: String(cabana?.brandColor || (type === 'principal' ?this.cabanaColor(item) : '#65758a')),
20704      };
20705    };
20706    const configured = [
20707      ...(Array.isArray(item?.cabanasPrincipales) ?item.cabanasPrincipales.map((cabana, index) => normalize(cabana, 'principal', index)) : []),
20708      ...(Array.isArray(item?.cabanasInvitadas) ?item.cabanasInvitadas.map((cabana, index) => normalize(cabana, 'invitada', index)) : []),
20709    ];
20710    if (configured.length) return configured;
20711    const cabins = Array.isArray(item?.cabins) ?item.cabins : [];
20712    return cabins
20713      .slice()
20714      .sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0))
20715      .map((cabana, index) => normalize(cabana, String(cabana?.type || '').toLowerCase() === 'principal' ?'principal' : 'invitada', index));
20716  }
20717  cabanaLogoReady(event) {
20718    const image = event?.currentTarget;
20719    const tile = image?.closest?.('.cabana-card-main-logo,.cabana-card-guest-logo');
20720    if (!image || !tile || !image.naturalWidth || !image.naturalHeight) return;
20721    try {
20722      const size = 40;
20723      const canvas = document.createElement('canvas');
20724      canvas.width = size;
20725      canvas.height = size;
20726      const context = canvas.getContext('2d', { willReadFrequently: true });
20727      if (!context) return;
20728      context.clearRect(0, 0, size, size);
20729      context.drawImage(image, 0, 0, size, size);
20730      const pixels = context.getImageData(0, 0, size, size).data;
20731      let luminance = 0;
20732      let alpha = 0;
20733      for (let index = 0; index < pixels.length; index += 4) {
20734        const opacity = pixels[index + 3] / 255;
20735        if (opacity < .08) continue;
20736        luminance += ((pixels[index] * .2126) + (pixels[index + 1] * .7152) + (pixels[index + 2] * .0722)) * opacity;
20737        alpha += opacity;
20738      }
20739      tile.classList.toggle('is-light-logo', alpha > 0 && (luminance / alpha) > 205);
20740    } catch {}
20741  }
20742  canCreateCabanaLot(item) {
20743    if (!item || !this.state.user || this.state.publicAdvanceMode) return false;
20744    const roles = this.apiRoleIds(this.state.apiUser);
20745    if (roles.some(role => ['admin', 'sysadmin'].includes(role))) return true;
20746    return roles.some(role => ['consignatario', 'certificador'].includes(role))
20747      && this.belongsToCabanaConsignataria(item)
20748      && this.cabanaMode(item) !== 'recent' && item?.load?.lotes !== false
20749      && String(item?.cargaLotes || 'abierta').toLowerCase() !== 'cerrada';
20750  }
20751  openCabanaLotCreate(item, updateHash = true) {
20752    if (!this.canCreateCabanaLot(item)) { this.showToast('No tenés habilitada la carga de lotes en este remate.'); return; }
20753    this.clearCabanaLotMedia();
20754    const schema = this.cabanaRemateSchema(item).filter((category) => Array.isArray(category?.forms));
20755    const first = schema[0];
20756    const firstCabana = this.cabanaRemateParticipants(item)[0];
20757    this.setState({ route: 'cabanaLotCreate', cabanaLotCreateRemate: { ...item, formsByCategory: schema }, cabanaLotCreateForm: { categoryId: String(first?.categoryId || first?.categoryName || ''), cabanaKey: firstCabana?.key || '', values: {}, observations: '' }, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null });
20758    if (updateHash) this.syncCabanaLocation(item, 'crear-lote');
20759    window.scrollTo(0, 0);
20760  }
20761  cabanaLotEditForm(remate, lot) {
20762    const schema = this.cabanaRemateSchema(remate).filter((category) => Array.isArray(category?.forms));
20763    const categoryId = String(lot?.categoryId || lot?.categoriaId || '');
20764    const categoryName = String(lot?.categoryName || lot?.categoria || '');
20765    const category = schema.find((item) =>
20765 categoryId && String(item?.categoryId || '') === categoryId)
20766      || schema.find((item) => categoryName && this.consKey(item?.categoryName) === this.consKey(categoryName))
20767      || schema[0];
20768    const categoryKey = String(category?.categoryId || category?.categoryName || 'category');
20769    const mainValues = lot?.values && typeof lot.values === 'object' ?lot.values : {};
20770    const subformValues = lot?.subformValues && typeof lot.subformValues === 'object' ?lot.subformValues : {};
20771    const values = {};
20772    (category?.forms || []).forEach((subform, formIndex) => {
20773      const formKey = String(subform?.key || subform?.formKey || this.rematePayloadKey(subform?.name || ('subformulario-' + formIndex)));
20774      const source = formIndex === 0 ?mainValues : (subformValues[formKey] || subformValues[this.consKey(formKey)] || subformValues[this.consKey(formKey).replace(/-/g, '_')] || subformValues[this.consKey(subform?.name || '')] || {});
20775      (subform?.fields || []).forEach((field, fieldIndex) => {
20776        const compositeKey = categoryKey + ':' + formIndex + ':' + fieldIndex;
20777        const fieldKey = String(field?.key || this.rematePayloadKey(field?.name || ('campo-' + fieldIndex)));
20778        let value = source?.[fieldKey];
20779        if (value === undefined) value = mainValues?.[fieldKey];
20780        if (value === undefined) value = lot?.[fieldKey];
20781        if (value === undefined && field?.isPrimaryIdentifier) value = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot);
20782        if (value !== undefined && value !== null) values[compositeKey] = value;
20783      });
20784    });
20785    const participants = this.cabanaRemateParticipants(remate);
20786    const selectedCabana = participants.find((cabana) => String(cabana.id || '') === String(lot?.cabanaId || ''))
20787      || participants.find((cabana) => this.consKey(cabana.name) === this.consKey(lot?.cabanaNombre || lot?.cabanaName || ''))
20788      || participants[0];
20789    return {
20790      schema,
20791      form: {
20792        categoryId: categoryKey,
20793        cabanaKey: selectedCabana?.key || '',
20794        values,
20795        observations: String(lot?.observations || lot?.observaciones || mainValues?.observaciones || ''),
20796      },
20797    };
20798  }
20799  async openCabanaLotEdit(item, lotOrCert, updatePath = true) {
20800    const remate = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
20801    const requestedCert = typeof lotOrCert === 'object' ?(this.cabanaPrebidCert(lotOrCert) || this.cabanaLotId(lotOrCert)) : String(lotOrCert || '').trim();
20802    const permissionLot = typeof lotOrCert === 'object' ?lotOrCert : { lotCert: requestedCert };
20803    if (!this.canEditCabanaLot(remate, permissionLot)) { this.showToast('No tenés permiso para editar este lote.'); return; }
20804    if (!requestedCert) { this.showToast('No se pudo identificar el lote.'); return; }
20805    if (updatePath) this.syncCabanaLocation(remate, 'lotes/' + encodeURIComponent(requestedCert) + '/editar');
20806    this.clearCabanaLotMedia();
20807    this.setState({ route: 'cabanaLotCreate', cabanaLotCreateRemate: remate, cabanaLotCreateLoading: true, cabanaLotEditingCert: requestedCert, cabanaLotEditingSource: null, cabanaLotImages: [], cabanaLotVideosUpload: [] });
20808    let lot = typeof lotOrCert === 'object' ?lotOrCert : null;
20809    if (!lot) {
20810      const sameAuction = this.cabanaRemateId(this.state.currentCabana) === this.cabanaRemateId(remate);
20811      const cached = sameAuction && Array.isArray(this.state.cabanaLots) ?this.state.cabanaLots : [];
20812      lot = cached.find((candidate) => this.cabanaPrebidCert(candidate) === requestedCert || this.cabanaLotId(candidate) === requestedCert) || null;
20813    }
20814    if (!lot) {
20815      const lots = await this.fetchCabanaLots(remate);
20816      lot = lots.find((candidate) => this.cabanaPrebidCert(candidate) === requestedCert || this.cabanaLotId(candidate) === requestedCert) || null;
20817    }
20818    if (!lot) {
20819      this.setState({ cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null });
20820      this.showToast('No encontramos el lote que querés editar.');
20821      this.openCabana(remate);
20822      return;
20823    }
20824    const prepared = this.cabanaLotEditForm(remate, lot);
20825    this.setState({
20826      route: 'cabanaLotCreate',
20827      cabanaLotCreateRemate: { ...remate, formsByCategory: prepared.schema },
20828      cabanaLotCreateForm: prepared.form,
20829      cabanaLotCreateLoading: false,
20830      cabanaLotEditingCert: requestedCert,
20831      cabanaLotEditingSource: lot,
20832    });
20833    window.scrollTo(0, 0);
20834  }
20835  async deleteCabanaLot(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) {
20836    if (!this.canDeleteCabanaLot(remate, lot)) { this.showToast('No tenés permiso para eliminar este lote.'); return; }
20837    const auctionId = this.cabanaRemateId(remate);
20838    const lotCert = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot);
20839    if (!auctionId || !lotCert || this.state.cabanaLotDeleting) return;
20840    const lotLabel = this.cabanaLotNumber(lot) || lotCert;
20841    if (!window.confirm('¿Eliminar el lote ' + lotLabel + '? Esta acción quitará el lote completo del remate.')) return;
20842    this.setState({ cabanaLotDeleting: lotCert });
20843    try {
20844      await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert), { method: 'DELETE' });
20845      this.flushCabanaLotViewTimer('lot_deleted');
20846      this.setState({ cabanaLotDeleting: '', currentCabanaLot: null, currentCabanaLotVariants: [] });
20847      this.showToast('Lote eliminado.');
20848      this.openCabana(remate);
20849    } catch (error) {
20850      this.setState({ cabanaLotDeleting: '' });
20851      this.showToast(error?.message || 'No se pudo eliminar el lote.');
20852    }
20853  }
20854  closeCabanaLotCreate() {
20855    this.clearCabanaLotMedia();
20856    const remate = this.state.cabanaLotCreateRemate;
20857    const editingCert = this.state.cabanaLotEditingCert;
20858    if (editingCert && remate) {
20859      this.setState({ cabanaLotCreateRemate: null, cabanaLotCreateForm: null, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null }, () => this.openCabana(remate, { focusLotCert: editingCert }));
20860      return;
20861    }
20862    this.syncAppLocation('cabanas');
20863    this.setState({ route: 'cabanas', cabanaLotCreateRemate: null, cabanaLotCreateForm: null, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null });
20864    window.scrollTo(0, 0);
20865  }
20866  setCabanaLotCabana(cabanaKey) { this.setState({ cabanaLotCreateForm: { ...(this.state.cabanaLotCreateForm || {}
20866), cabanaKey: String(cabanaKey || '') } }); }
20867  setCabanaLotCategory(categoryId) { this.setState({ cabanaLotCreateForm: { ...(this.state.cabanaLotCreateForm || {}), categoryId: String(categoryId || '') } }); }
20868  setCabanaLotValue(key, value, buffered = false) {
20869    const form = this.state.cabanaLotCreateForm || { values: {} };
20870    const patch = { cabanaLotCreateForm: { ...form, values: { ...(form.values || {}), [key]: value } } };
20871    if (buffered) this.bufferInputState('cabana-lot-' + key, patch);
20872    else this.setState(patch);
20873  }
20874  addCabanaLotMedia(kind, files) {
20875    const isVideo = kind === 'video';
20876    const stateKey = isVideo ?'cabanaLotVideosUpload' : 'cabanaLotImages';
20877    const limit = isVideo ?3 : 8;
20878    const current = this.state[stateKey] || [];
20879    const maxVideoSize = 500 * 1024 * 1024;
20880    const candidates = Array.from(files || []);
20881    const accepted = candidates.filter((file) => isVideo
20882      ?file.type.startsWith('video/') && file.size <= maxVideoSize
20883      : file.type.startsWith('image/')).slice(0, Math.max(0, limit - current.length));
20884    if (isVideo && candidates.some((file) => file.size > maxVideoSize)) {
20885      this.showToast('Cada video puede pesar como máximo 500 MB.');
20886    }
20887    if (!accepted.length) { this.showToast(current.length >= limit ?'Alcanzaste el máximo de archivos.' : 'Seleccioná archivos válidos.'); return; }
20888    const media = accepted.map((file) => ({ id: kind + '-' + Date.now() + '-' + Math.random().toString(36).slice(2), file, name: file.name, size: file.size, url: URL.createObjectURL(file) }));
20889    this.setState({ [stateKey]: [...current, ...media] });
20890  }
20891  removeCabanaLotMedia(kind, id) {
20892    const stateKey = kind === 'video' ?'cabanaLotVideosUpload' : 'cabanaLotImages';
20893    const current = this.state[stateKey] || [];
20894    const removed = current.find((media) => media.id === id);
20895    if (removed) { try { URL.revokeObjectURL(removed.url); } catch {} }
20896    this.setState({ [stateKey]: current.filter((media) => media.id !== id) });
20897  }
20898  async uploadCabanaLotVideos(auctionId, lotCert, videos, metadata) {
20899    const uploadedMediaIds = [];
20900    const associatedLotIds = [...new Set((metadata.associatedLotIds || []).map(String))];
20901    for (let index = 0; index < videos.length; index++) {
20902      const media = videos[index];
20903      const file = media?.file;
20904      if (!file) continue;
20905      try {
20906        this.showToast('Subiendo video ' + (index + 1) + ' de ' + videos.length + '…');
20907        const common = {
20908          videoType: metadata.videoType === 'final' ? 'final' : 'raw',
20909          associatedLotIds,
20910          cabanaName: metadata.cabanaName,
20911          primaryIdentifierKey: metadata.primaryIdentifierKey,
20912          primaryIdentifierName: metadata.primaryIdentifierName,
20913          primaryIdentifierValue: metadata.primaryIdentifierValue,
20914          lotLabel: metadata.lotLabel,
20915          paintLabel: metadata.paintLabel || null,
20916        };
20917        const upload = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/videos/upload-url', {
20918          method: 'POST',
20919          body: JSON.stringify({ fileName: file.name, contentType: file.type || 'application/octet-stream', sizeBytes: file.size, ...common }),
20920        });
20921        const uploadUrl = upload?.uploadUrl || upload?.signedUrl;
20922        const fileKey = upload?.fileKey || upload?.key;
20923        if (!uploadUrl || !fileKey) throw new Error('La API no devolvió los datos necesarios para subir el video.');
20924        await this.uploadSignedFile(uploadUrl, file, (progress) => metadata.onProgress?.(progress));
20925        metadata.onProcessing?.();
20926        await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/videos/metadata', {
20927          method: 'POST',
20928          body: JSON.stringify({
20929            fileKey,
20930            fileName: file.name,
20931            contentType: file.type || 'application/octet-stream',
20932            sizeBytes: file.size,
20933            order: Number(upload?.order || index + 1),
20934            ...common,
20935          }),
20936        });
20937        uploadedMediaIds.push(media.id);
20938      } catch (error) {
20939        error.uploadedMediaIds = uploadedMediaIds;
20940        throw error;
20941      }
20942    }
20943    return uploadedMediaIds;
20944  }
20945  async quickUploadCabanaLotVideo(event, targetLot = null) {
20946    const input = event?.target;
20947    const files = Array.from(input?.files || []);
20948    if (input) input.value = '';
20949    const remate = this.state.currentCabana;
20950    const lot = targetLot || this.state.currentCabanaLot;
20951    const cardUpload = !!targetLot;
20952    const cardUploadKey = this.cabanaLotId(lot);
20953    const currentCardUpload = this.state.cabanaCardVideoUploads?.[cardUploadKey] || {};
20954    if (!files.length || (cardUpload ?currentCardUpload.busy : this.state.cabanaLotQuickVideoUploading)) return;
20955    if (!this.canUploadCabanaLotMedia(remate, lot)) { this.showToast('No tenés permiso para subir videos a este remate.'); return; }
20956    if (files.some(file => !String(file.type || '').startsWith('video/') && !/\.(mp4|mov|m4v|webm)$/i.test(file.name || ''))) { this.showToast('Seleccioná un archivo de video válido.'); return; }
20957    if (files.some(file => file.size > 500 * 1024 * 1024)) { this.showToast('El video puede pesar como máximo 500 MB.'); return; }
20958    const auctionId = this.cabanaRemateId(remate);
20959    const lotCert = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot);
20960    if (!auctionId || !lotCert) { this.showToast('No pudimos identificar el remate o el lote.'); return; }
20961    let videoOptions;
20962    try {const type = input?.dataset?.videoType; if (type === 'final' && !this.canUploadCabanaFinal(remate)) throw new Error('No tenés permiso para cargar videos finales.'); videoOptions = ['raw', 'final'].includes(type) ? {videoType:type, associatedLotIds:[lotCert]} : await this.chooseCabanaVideoOptions(remate,lotCert);if(!videoOptions)return;} catch(error){this.showToast(error?.data?.detail || error.message);return;}
20963    if (videoOptions.videoType === 'final' && files.length !== 1) { this.showToast('Seleccioná un solo video final. Para varios archivos elegí Crudos.'); return; }
20964    const participants = this.cabanaRemateParticipants(remate);
20965    const selectedCabana = participants.find((cabana) => String(cabana.id || '') === String(lot?.cabanaId || ''))
20966      || participants.find((cabana) => this.consKey(cabana.name) === this.consKey(lot?.cabanaNombre || lot?.cabanaName || ''))
20967      || participants[0] || {};
20968    const values = lot?.values || {};
20969    const isCorral = Object.keys(values).some((key) => this.consKey(key) === 'corral') || /corral/i.test(this.cabanaLotNumber(lot));
20970    const identifier = String((isCorral ?values.corral : (values.rp || lot?.rp)) || this.cabanaPrebidAnimalCert(lot) || this.cabanaLotNumber(lot) || lotCert).trim();
20971    const primaryIdentifierKey = isCorral ?'corral' : 'rp';
20972    const updateProgress = (progress, stage, busy = true) => cardUpload
20973      ?this.setState((state) => ({ cabanaCardVideoUploads: { ...(state.cabanaCardVideoUploads || {}), [cardUploadKey]: { busy, progress, stage } } }))
20974      :this.setState({ cabanaLotQuickVideoUploading: busy, cabanaLotQuickVideoProgress: progress, cabanaLotQuickVideoStage: stage });
20975    updateProgress(0, 'Preparando la carga…');
20976    try {
20977      await this.uploadCabanaLotVideos(auctionId, lotCert, files.map((file,index) => ({ id: 'quick-' + Date.now() + '-' + index, file })), {
20978        ...videoOptions,
20979        onProgress: (progress) => updateProgress(progress, 'Subiendo video…'),
20980        onProcessing: () => updateProgress(100, 'Procesando portada…'),
20981        cabanaName: lot?.cabanaNombre || lot?.cabanaName || selectedCabana.name || 'Cabaña',
20982        primaryIdentifierKey,
20983        primaryIdentifierName: isCorral ?'Corral' : 'RP',
20984        primaryIdentifierValue: identifier,
20985        lotLabel: (isCorral ?'Corral ' : 'Lote ') + identifier,
20986        paintLabel: String(values.pintura || lot?.pintura || '').trim(),
20987      });
20988      await this.fetchCabanaLotVideos({ ...lot, __videosLoaded: false });
20989      this.showToast(files.length === 1 ? 'Video subido correctamente.' : files.length + ' videos subidos correctamente.');
20990    } catch (error) {
20991      this.showToast(error?.message || 'No se pudo subir el video.');
20992    } finally {
20993      updateProgress(0, '', false);
20994    }
20995  }
20996  async deleteCabanaLotVideo(remate, lot, video, event = null) {
20997    event?.preventDefault?.();
20998    event?.stopPropagation?.();
20999    const videoId = String(video?.id || '').trim();
21000    const auctionId = this.cabanaRemateId(remate);
21001    const lotCert = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot);
21002    if (!videoId || !auctionId || !lotCert || this.state.cabanaLotVideoDeleting) return;
21003    if (!this.canExportCabanaCatalog(remate, this.state.apiUser)) { this.showToast('No tenés permiso para eliminar videos de este remate.'); return; }
21004    const shared=[...new Set([lotCert,...(video.associatedLotIds || [])].map(String))];
21005    if (!window.confirm('¿Eliminar este video? '+(shared.length > 1 ?'Se eliminará el archivo compartido de todos los lotes asociados ('+shared.join(', ')+'), no solamente de este lote. ':'')+'Esta acción no se puede deshacer.')) return;
21006    this.setState({ cabanaLotVideoDeleting: videoId });
21007    try {
21008      await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/videos/' + encodeURIComponent(videoId), { method: 'DELETE' });
21009      const refreshedLots = await this.fetchCabanaLots(remate);
21010      const targetItemId = String(lot?.itemId || '');
21011      const refreshedLot = refreshedLots.find((item) => this.cabanaLotId(item) === String(lotCert) && (!targetItemId || String(item?.itemId || '') 
21011=== targetItemId));
21012      if (this.cabanaLotId(this.state.currentCabanaLot) === String(lotCert)) {
21013        this.setState({ currentCabanaLot: refreshedLot || { ...lot, videos: [], coverImage: null, lotCoverImage: null }, cbLotVideo: 0, cabanaLotMediaReady: false, cabanaLotMediaFailed: false });
21014      }
21015      this.showToast('Video eliminado correctamente.');
21016    } catch (error) {
21017      this.showToast(error?.message || 'No se pudo eliminar el video.');
21018    } finally {
21019      this.setState({ cabanaLotVideoDeleting: '' });
21020    }
21021  }
21022  async saveCabanaLotDraft() {
21023    const remate = this.state.cabanaLotCreateRemate;
21024    const form = this.state.cabanaLotCreateForm || {};
21025    const editingCert = String(this.state.cabanaLotEditingCert || '').trim();
21026    const editingSource = this.state.cabanaLotEditingSource;
21027    if (this.state.cabanaLotCreateSaving) return;
21028    if (editingCert && !this.canEditCabanaLot(remate, editingSource || { lotCert: editingCert })) {
21029      this.showToast('No tenés permiso para editar este lote.');
21030      return;
21031    }
21032    if (!editingCert && !this.canCreateCabanaLot(remate)) {
21033      this.showToast('No tenés habilitada la carga de lotes en este remate.');
21034      return;
21035    }
21036    const cabanas = this.cabanaRemateParticipants(remate);
21037    const selectedCabana = cabanas.find((cabana) => cabana.key === String(form.cabanaKey || ''));
21038    if (!selectedCabana) { this.showToast('Seleccioná la cabaña a la que pertenece el lote.'); return; }
21039    if (!form.categoryId) { this.showToast('Seleccioná una categoría.'); return; }
21040    const schema = this.cabanaRemateSchema(remate);
21041    const category = schema.find((item) => String(item.categoryId || item.categoryName) === String(form.categoryId));
21042    const categoryValueKey = String(category?.categoryId || category?.categoryName || 'category');
21043    const fields = (category?.forms || []).flatMap((subform, formIndex) => (subform.fields || []).map((field, fieldIndex) => ({ field, key: categoryValueKey + ':' + formIndex + ':' + fieldIndex })));
21044    const primaryField = fields.find((item) => item.field.isPrimaryIdentifier);
21045    const primaryKey = primaryField?.key || '';
21046    const identifier = String(primaryKey ?form.values?.[primaryKey] || '' : '').trim();
21047    if (!identifier) { this.showToast('Completá el identificador principal del lote.'); return; }
21048    const mainValues = {};
21049    const subforms = [];
21050    (category?.forms || []).forEach((subform, formIndex) => {
21051      const values = {};
21052      (subform.fields || []).forEach((field, fieldIndex) => {
21053        const compositeKey = categoryValueKey + ':' + formIndex + ':' + fieldIndex;
21054        const value = form.values?.[compositeKey];
21055        if (value === '' || value === null || value === undefined) return;
21056        const fieldKey = String(field?.key || this.rematePayloadKey(field?.name || ('campo-' + fieldIndex)));
21057        values[fieldKey] = value;
21058      });
21059      if (!Object.keys(values).length) return;
21060      if (formIndex === 0) Object.assign(mainValues, values);
21061      else subforms.push({ formKey: String(subform?.key || this.rematePayloadKey(subform?.name || ('subformulario-' + formIndex))), values });
21062    });
21063    const auctionId = this.cabanaRemateId(remate);
21064    const editingItemId = String(editingSource?.itemId || editingSource?.cert || '').trim();
21065    const itemPayload = {
21066      ...(editingCert && editingItemId ?{ id: editingItemId } : {}),
21067      cert: identifier,
21068      displayOrder: editingCert ?Number(editingSource?.itemDisplayOrder || editingSource?.displayOrder || 1) : 1,
21069      values: mainValues,
21070      subforms,
21071      ...(String(form.observations || '').trim() ?{ observations: String(form.observations).trim() } : {}),
21072    };
21073    const editingItems = editingCert
21074      ?(this.state.cabanaLots || []).filter((lot) => this.cabanaPrebidCert(lot) === editingCert).map((lot) => {
21075        const lotItemId = String(lot?.itemId || lot?.id || '').trim();
21076        const isEditingItem = editingItemId ?lotItemId === editingItemId : lot === editingSource;
21077        if (isEditingItem) return itemPayload;
21078        return {
21079          ...(lotItemId ?{ id: lotItemId } : {}),
21080          cert: this.cabanaPrebidAnimalCert(lot) || lotItemId,
21081          displayOrder: Number(lot?.itemDisplayOrder || lot?.displayOrder || 1),
21082          values: lot?.values && typeof lot.values === 'object' ?lot.values : {},
21083          subforms: Array.isArray(lot?.subforms) ?lot.subforms : [],
21084          ...(String(lot?.observations || lot?.observaciones || '').trim() ?{ observations: String(lot?.observations || lot?.observaciones).trim() } : {}),
21085        };
21086      })
21087      : [];
21088    const payload = {
21089      updatePrebidPricing: false,
21090      categoryId: category?.categoryId || null,
21091      categoryName: category?.categoryName || '',
21092      cabanaId: editingCert ?(editingSource?.cabanaId || selectedCabana.id || null) : (selectedCabana.id || null),
21093      cabanaName: selectedCabana.name || '',
21094      displayOrder: editingCert ?Number(editingSource?.groupDisplayOrder || editingSource?.displayOrder || 1) : 1,
21095      items: editingItems.length ?editingItems : [itemPayload],
21096    };
21097    const primaryIdentifierKey = String(primaryField?.field?.key || this.rematePayloadKey(primaryField?.field?.name || '')).trim();
21098    const primaryIdentifierName = String(primaryField?.field?.name || primaryIdentifierKey).trim();
21099    const paintField = fields.find((item) => this.consKey(item.field?.key || item.field?.name) === 'pintura');
21100    const paintLabel = String(paintField ?form.values?.[paintField.key] || '' : '').trim();
21101    const videoMetadata = {
21102      cabanaName: selectedCabana.name || '',
21103      primaryIdentifierKey,
21104      primaryIdentifierName,
21105      primaryIdentifierValue: identifier,
21106      lotLabel: (this.consKey(primaryIdentifierName).includes('corral') ?'Corral ' : 'Lote ') + identifier,
21107      paintLabel,
21108    };
21109    const pendingVideos = [...(this.state.cabanaLotVideosUpload || [])];
21110    if (pendingVideos.length) {try {const options=await this.chooseCabanaVideoOptions(remate,editingCert);if(!options)return;if(options.videoType==='final' && pendingVideos.length!==1){this.showToast('Seleccioná un solo video final. Para varios archivos elegí Crudos.');return;}Object.assign(videoMetadata,options);}catch(error){this.showToast(error?.data?.detail || error.message);return;}}
21111    let savedLotCert = editingCert;
21112    let savedLot = null;
21113    this.setState({ cabanaLotCreateSaving: true });
21114    try {
21115      const path = '/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots' + (editingCert ?'/' + encodeURIComponent(editingCert) : '');
21116      savedLot = await this.cabanaApiFetch(path, { method: editingCert ?'PATCH' : 'POST', body: JSON.stringify(payload) });
21117      savedLotCert = String(savedLot?.lotCert || savedLot?.id || editingCert || '').trim();
21118      if (!savedLotCert) throw new Error('El lote se guardó, pero la API no devolvió su identificador para subir videos.');
21119      if (pendingVideos.length) await this.uploadCabanaLotVideos(auctionId, savedLotCert, pendingVideos, videoMetadata);
21120      this.showToast(editingCert ?'Lote actualizado.' : 'Lote creado en el remate.');
21121      this.clearCabanaLotMedia();
21122      if (editingCert) {
21123        const lots = await this.fetchCabanaLots(remate);
21124        const updatedLot = lots.find((lot) =>
21124 editingItemId && String(lot?.itemId || '') === editingItemId && (this.cabanaPrebidCert(lot) === editingCert || this.cabanaLotId(lot) === editingCert))
21125          || lots.find((lot) => this.cabanaPrebidCert(lot) === editingCert || this.cabanaLotId(lot) === editingCert)
21126          || null;
21127        this.setState({
21128          cabanaLotCreateSaving: false,
21129          cabanaLotCreateRemate: null,
21130          cabanaLotCreateForm: null,
21131          cabanaLotEditingCert: '',
21132          cabanaLotEditingSource: null,
21133          cabanaLotImages: [],
21134          cabanaLotVideosUpload: [],
21135          currentCabanaLot: updatedLot,
21136        }, () => {
21137          if (updatedLot) {
21138            const groupKey = this.cabanaLotGroupKey(updatedLot);
21139            const variants = lots.filter((lot) => this.cabanaLotGroupKey(lot) === groupKey);
21140            this.openCabanaLot(updatedLot, variants.length ?variants : [updatedLot]);
21141          }
21142          else this.openCabana(remate, { focusLotCert: editingCert });
21143        });
21144      } else {
21145        this.setState({ cabanaLotCreateSaving: false, cabanaLotCreateRemate: null, cabanaLotCreateForm: null, route: 'cabanas', cabanaLotEditingCert: '', cabanaLotEditingSource: null });
21146        try { window.location.hash = '#/cabanas'; } catch {}
21147        await this.fetchCabanas(true);
21148      }
21149    } catch (error) {
21150      const normalizedSaved = savedLot ?this.normalizeCabanaAuctionLots([savedLot], remate)[0] : null;
21151      this.setState({
21152        cabanaLotCreateSaving: false,
21153        ...(error?.uploadedMediaIds?.length ?{
21154          cabanaLotVideosUpload: (this.state.cabanaLotVideosUpload || []).filter((media) => !error.uploadedMediaIds.includes(media.id)),
21155        } : {}),
21156        ...(!editingCert && savedLotCert ?{ cabanaLotEditingCert: savedLotCert, cabanaLotEditingSource: normalizedSaved || { ...payload, lotCert: savedLotCert } } : {}),
21157      });
21158      const savedButUploadFailed = !!savedLot;
21159      this.showToast(savedButUploadFailed
21160        ?'El lote quedó guardado, pero el video no pudo subirse: ' + (error?.message || 'intentá nuevamente.')
21161        : (error?.message || (editingCert ?'No se pudo actualizar el lote.' : 'No se pudo crear el lote.')));
21162    }
21163  }
21164  async fetchCabanas(force = false) {
21165    if (this._cabanasRequest) return this._cabanasRequest;
21166    const cacheKey = 'rosgan:cabanas:v2';
21167    const cached = !force && !Array.isArray(this.state.cabanas) ?this.readCollectionCache(cacheKey) : null;
21168    const fallback = Array.isArray(cached) ?cached : this.state.cabanas;
21169    const fallbackIsComplete = Array.isArray(cached) || !!this.state.cabanasCollectionReady;
21170    this.setState({
21171      cabanas: Array.isArray(fallback) ?fallback : this.state.cabanas,
21172      cabanasLoading: true,
21173      cabanasCollectionReady: fallbackIsComplete,
21174      cabanasError: '',
21175    }, () => {
21176      if (fallbackIsComplete && this.state.route === 'cabanas') this.fetchCabanaCardMedia(fallback);
21177    });
21178    const request = (async () => {
21179      try {
21180        const data = await this.cabanaApiFetch('/api/cabin-auctions');
21181        const items = this.listFrom(data, ['items', 'data', 'auctions', 'cabinAuctions']).map((item) => this.normalizeCabanaAuctionRecord(item)).filter((item) => !this.isExcludedCabana(item)).sort((a, b) => String(b?.updatedAt || b?.createdAt || '').localeCompare(String(a?.updatedAt || a?.createdAt || '')));
21182        this.writeCollectionCache(cacheKey, items, 30 * 60 * 1000);
21183        this.setState({ cabanas: items, cabanasLoading: false, cabanasCollectionReady: true, cabanasError: '' }, () => {
21184          this.loadRemateListCatalogs(items, 'cabana');
21185          if (this.state.route === 'cabanas') this.fetchCabanaCardMedia(items);
21186        });
21187        return items;
21188      } catch (error) {
21189        this.setState({
21190          cabanas: Array.isArray(this.state.cabanas) ?this.state.cabanas : (fallback || []),
21191          cabanasLoading: false,
21192          cabanasCollectionReady: fallbackIsComplete,
21193          cabanasError: error?.message || 'No se pudieron cargar los remates de cabaña.',
21194        });
21195        return null;
21196      }
21197    })();
21198    this._cabanasRequest = request;
21199    try { return await request; }
21200    finally { if (this._cabanasRequest === request) this._cabanasRequest = null; }
21201  }
21202  cabanaCardMediaFrames(urls = []) {
21203    const unique = [...new Set((urls || []).map(window.resolveCabanaMediaUrl).filter(
21203Boolean))].slice(0, 8);
21204    if (!unique.length) return [];
21205    return Array.from({ length: 8 }, (_, index) => ({
21206      key: index + ':' + unique[index % unique.length],
21207      src: unique[index % unique.length],
21208      delay: (-index * 5) + 's',
21209    }));
21210  }
21211  async fetchCabanaCardMedia(items = this.state.cabanas || []) {
21212    const auctions = (Array.isArray(items) ?items : []).filter((item) => this.cabanaRemateId(item));
21213    if (!auctions.length) return;
21214    if (!this._cabanaCardMediaRequests) this._cabanaCardMediaRequests = new Set();
21215    const pending = auctions.filter((item) => {
21216      const id = this.cabanaRemateId(item);
21217      return !this.state.cabanaCardMedia?.[id]?.ready && !this._cabanaCardMediaRequests.has(id);
21218    });
21219    await Promise.all(pending.map(async (item) => {
21220      const id = this.cabanaRemateId(item);
21221      this._cabanaCardMediaRequests.add(id);
21222      try {
21223        const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(id) + '/lots/');
21224        const groups = this.listFrom(data, ['data', 'items', 'lots', 'lotes', 'groups']);
21225        const lots = this.normalizeCabanaAuctionLots(groups, item);
21226        const videoThumbs = lots.flatMap((lot) => this.cabanaLotVideos(lot).map((video) =>
21226 video.thumb).filter(Boolean));
21227        const coverImages = lots.flatMap((lot) => this.cabanaLotImages(lot).map((image) => image.url).filter(Boolean));
21228        const frames = this.cabanaCardMediaFrames([...videoThumbs, ...coverImages]);
21229        this.setState((state) => ({ cabanaCardMedia: { ...(state.cabanaCardMedia || {}), [id]: { ready: true, frames } } }));
21230      } catch {
21231        this.setState((state) => ({ cabanaCardMedia: { ...(state.cabanaCardMedia || {}), [id]: { ready: true, frames: [] } } }));
21232      } finally {
21233        this._cabanaCardMediaRequests.delete(id);
21234      }
21235    }));
21236  }
21237  async fetchCabanaById(id, options = {}) {
21238    const cleanId = String(id || '').trim();
21239    if (!cleanId) return null;
21240    const existing = (this.state.cabanas || []).find((item) => this.cabanaRemateId(item) === cleanId);
21241    if (existing?.__detailLoaded && !options.force) return existing;
21242    const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(cleanId));
21243    const item = this.normalizeCabanaAuctionRecord(data?.data || data?.item || data?.auction || data?.cabinAuction || data);
21244    if (!item || this.isExcludedCabana(item)) return null;
21245    const itemId = this.cabanaRemateId(item) || cleanId;
21246    const merged = { ...item, id: item?.id || itemId, __detailLoaded: true, __detailLoadedAt: Date.now() };
21247    this.setState({ cabanas: [merged, ...(this.state.cabanas || []).filter((entry) => this.cabanaRemateId(entry) !== itemId)] });
21248    return merged;
21249  }
21250  async fetchCabanaAnalyticsSummary(item = this.state.currentCabana) {
21251    const auctionId = this.cabanaRemateId(item);
21252    if (!auctionId) return null;
21253    this.setState({ cabanaAnalyticsLoading: true });
21254    try {
21255      const summary = await this.fetchCabanaAnalyticsSummaryById(auctionId);
21256      if (this.cabanaRemateId(this.state.currentCabana) === auctionId) {
21257        this.setState({ cabanaAnalyticsSummary: summary, cabanaAnalyticsLoading: false });
21258      }
21259      return summary;
21260    } catch (error) {
21261      if (this.cabanaRemateId(this.state.currentCabana) === auctionId) {
21262        this.setState({ cabanaAnalyticsSummary: null, cabanaAnalyticsLoading: false });
21263      }
21264      return null;
21265    }
21266  }
21267  async fetchCabanaAnalyticsSummaryById(auctionId) {
21268    const cleanId = String(auctionId || '').trim();
21269    if (!cleanId) return null;
21270    const token = await this.authToken(false);
21271    const response = await fetch('/api/cabanas-analytics/summary?auctionId=' + encodeURIComponent(cleanId) + '&limit=500', {
21272      headers: { accept: 'application/json', Authorization: 'Bearer ' + token },
21273      cache: 'no-store',
21274    });
21275    if (!response.ok) throw new Error('Analytics HTTP ' + response.status);
21276    return response.json();
21277  }
21278  cabanaSalesCategoryKey(lot) {
21279    return String(lot?.categoryId || this.consKey(lot?.categoryName || 'sin-categoria'));
21280  }
21281  cabanaSalesCategoryName(lot) {
21282    return String(lot?.categoryName || 'Sin categoría');
21283  }
21284  cabanaCategoryDisplayOrder(lot, fallback = Number.MAX_SAFE_INTEGER) {
21285    const candidates = [
21286      lot?.categoryDisplayOrder,
21287      lot?.category_display_order,
21288      lot?.categoryOrder,
21289      lot?.category?.displayOrder,
21290      lot?.category?.categoryDisplayOrder,
21291      lot?.categoria?.displayOrder,
21292      lot?.categoria?.categoryDisplayOrder,
21293    ];
21294    for (const value of candidates) {
21295      const parsed = Number(value);
21296      if (Number.isFinite(parsed)) return parsed;
21297    }
21298    return fallback;
21299  }
21300  cabanaLotSortNumber(value, fallback = Number.MAX_SAFE_INTEGER) {
21301    const parsed = Number(value);
21302    return Number.isFinite(parsed) && parsed >= 0 ?parsed : fallback;
21303  }
21304  cabanaSortLotsByCategoryOrder(lots) {
21305    return (Array.isArray(lots) ?lots : []).slice().sort((a, b) => {
21306      const categoryOrder = this.cabanaCategoryDisplayOrder(a) - this.cabanaCategoryDisplayOrder(b);
21307      if (categoryOrder) return categoryOrder;
21308      const groupOrder = this.cabanaLotSortNumber(a?.displayOrder ?? a?.groupDisplayOrder) - this.cabanaLotSortNumber(b?.displayOrder ?? b?.groupDisplayOrder);
21309      if (groupOrder) return groupOrder;
21310      const lotOrder = String(this.cabanaLotNumber(a)).localeCompare(String(this.cabanaLotNumber(b)), 'es', { numeric: true, sensitivity: 'base' });
21311      if (lotOrder) return lotOrder;
21312      const itemOrder = this.cabanaLotSortNumber(a?.itemDisplayOrder ?? a?.displayOrder) - this.cabanaLotSortNumber(b?.itemDisplayOrder ?? b?.displayOrder);
21313      if (itemOrder) return itemOrder;
21314      return String(this.cabanaAnalyticsRpId(a)).localeCompare(String(this.cabanaAnalyticsRpId(b)), 'es', { numeric: true, sensitivity: 'base' });
21315    });
21316  }
21317  normalizeCabanaSalesOrder(lots) {
21318    return (Array.isArray(lots) ?lots : []).map((lot, index) => ({ ...lot, displayOrder: index + 1 }));
21319  }
21320  sortedCabanaSalesOrder(lots) {
21321    const displayOrderOf = (lot) => {
21322      const value = Number(lot?.displayOrder);
21323      return Number.isFinite(value) && value > 0 ?value : Number.MAX_SAFE_INTEGER;
21324    };
21325    return this.normalizeCabanaSalesOrder([...(Array.isArray(lots) ?lots : [])].sort((a, b) => displayOrderOf(a) - displayOrderOf(b)));
21326  }
21327  openCabanaSalesOrder(lotsOverride = null) {
21328    this.openCabanaSalesOrderPage(this.state.currentCabana, lotsOverride, true);
21329  }
21330  openCabanaSalesOrderPage(item, lotsOverride = null, syncHash = true) {
21331    if (!this.canManageUsers() || !item) return;
21332    item = this.normalizeCabanaAuctionRecord(item);
21333    const auctionId = this.cabanaRemateId(item);
21334    const applyLots = (sourceLots) => {
21335      const lots = this.sortedCabanaSalesOrder(sourceLots);
21336      this.setState({
21337        route: 'cabanaSalesOrder', currentCabana: item, cabanaLots: Array.isArray(sourceLots) ?sourceLots : [],
21338        cabanaLotsLoading: false, cabanaSalesOrderOpen: false, cabanaSalesOrderGrouped: false,
21339        cabanaSalesOrderLots: lots, cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false,
21340      });
21341      window.scrollTo(0, 0);
21342    };
21343    if (syncHash && auctionId) this.syncCabanaLocation(item, 'orden-ventas');
21344    if (Array.isArray(lotsOverride)) {
21345      applyLots(lotsOverride);
21346      return;
21347    }
21348    const currentId = this.cabanaRemateId(this.state.currentCabana);
21349    if (currentId === auctionId && Array.isArray(this.state.cabanaLots)) {
21350      applyLots(this.state.cabanaLots);
21351      return;
21352    }
21353    this.setState({
21354      route: 'cabanaSalesOrder', currentCabana: item, cabanaLotsLoading: true,
21355      cabanaSalesOrderLots: [], cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false,
21356    });
21357    window.scrollTo(0, 0);
21358    this.fetchCabanaLots(item).then(applyLots).catch((error) => {
21359      this.setState({ cabanaLotsLoading: false, cabanaSalesOrderError: error?.message || 'No se pudieron cargar los lotes.' });
21360    });
21361  }
21362  closeCabanaSalesOrder() {
21363    if (this.state.cabanaSalesOrderSaving) return;
21364    this.destroyCabanaSalesOrderSorting();
21365    const id = this.cabanaRemateId(this.state.currentCabana);
21366    this.setState({ route: 'cabanaDetail', cabanaSalesOrderOpen: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false });
21367    if (id) this.syncCabanaLocation(this.state.currentCabana);
21368    window.scrollTo(0, 0);
21369  }
21370  cabanaCommercialStorageKey(remate = this.state.currentCabana) {
21371    return 'rosgan:cabanaCommercial:' + (this.cabanaRemateId(remate) || 'lacassina');
21372  }
21373  loadCabanaCommercialDrafts(remate = this.state.currentCabana) {
21374    try {
21375      const data = JSON.parse(localStorage.getItem(this.cabanaCommercialStorageKey(remate)) || '{}');
21376      return data && typeof data === 'object' ?data : {};
21377    } catch {
21378      return {};
21379    }
21380  }
21381  saveCabanaCommercialDrafts(remate = this.state.currentCabana, drafts = this.state.cabanaCommercialDrafts) {
21382    try {
21383      localStorage.setItem(this.cabanaCommercialStorageKey(remate), JSON.stringify(drafts || {}));
21384      return true;
21385    } catch {
21386      return false;
21387    }
21388  }
21389  cabanaCommercialRowKey(lot) {
21390    const lotCert = String(lot?.lotCert || lot?.groupId || this.cabanaLotId(lot) || '').trim();
21391    const itemId = String(lot?.itemId || '').trim();
21392    return [lotCert, itemId].filter(Boolean).join(':') || [this.cabanaLotNumber(lot), this.cabanaAnalyticsRpId(lot)].filter(Boolean).join('-') || String(lot?.id || '');
21393  }
21394  normalizeCabanaCommercialRecord(commercial) {
21395    if (!commercial || typeof commercial !== 'object') return {};
21396    const statusMap = { sold: 'vendido', pending: 'pendiente', unsold: 'no-vendido', not_sold: 'no-vendido', lifted: 'levantado' };
21397    const rawStatus = String(commercial.status || commercial.estado || 'pendiente').trim().toLowerCase().replace(/_/g, '-');
21398    const rawMode = String(commercial.priceMode || commercial.priceType || commercial.tipoPrecio || commercial.price_type || '').toLowerCase();
21399    const first = (...values) => values.find((value) => value !== undefined && value !== null && value !== '');
21400    return {
21401      status: statusMap[rawStatus] || rawStatus || 'pendiente',
21402      priceMode: /kilo|kg/.test(rawMode) ?'kilo' : 'cabeza',
21403      precioKilo: commercial.divisions?.length?String(Math.round(Number(first(commercial.pricePerKilo,commercial.precioKilo,0)))):String(first(commercial.pricePerKilo, commercial.pricePerKg, commercial.precioKilo, commercial.precio_kilo, '') || ''),
21404      precioCabeza: commercial.divisions?.length?String(Math.round(Number(first(commercial.pricePerHead,commercial.precioCabeza,0)))):String(first(commercial.pricePerHead, commercial.precioCabeza, commercial.precio_cabeza, '') || ''),
21405      faenaEnabled: !!first(commercial.slaughter, commercial.isSlaughter, commercial.faena, commercial.faenaEnabled, false),
21406      faenaPercent: String(first(commercial.slaughterPercentage, commercial.faenaPercentage, commercial.faenaPercent, commercial.porcentajeFaena, '') || ''),
21407      cantidad: String(first(commercial.quantity, commercial.cantidad, '') || ''),
21408      pesoProm: String(first(commercial.averageWeight, commercial.pesoPromedio, commercial.pesoProm, '') || ''),
21409      plazo: String(first(commercial.paymentTerm, commercial.term, commercial.plazo, '') || ''),
21410      comprador: String(first(commercial.buyer, commercial.comprador, '') || ''),
21411      divisions: Array.isArray(commercial.divisions)?commercial.divisions:[],
21412      updatedAt: first(commercial.updatedAt, commercial.updated_at, new Date().toISOString()),
21413    };
21414  }
21415  cabanaCommercialPayload(lot, draft = {}) {
21416    const digits = (value) => String(value ?? '').replace(/\D/g, '');
21417    const numberOrNull = (value) => { const clean = digits(value); return clean ?Number(clean) : null; };
21418    const numericTextOrNull = (value) => digits(value) || null;
21419    return {
21420      lotCert: String(lot?.lotCert || lot?.groupId || this.cabanaLotId(lot) || ''),
21421      itemId: String(lot?.itemId || lot?.id || ''),
21422      status: draft.status || 'pendiente',
21423      priceMode: draft.priceMode === 'kilo' ?'kilo' : 'cabeza',
21424      pricePerKilo: numberOrNull(draft.precioKilo),
21425      pricePerHead: numberOrNull(draft.precioCabeza),
21426      slaughter: !!draft.faenaEnabled,
21427      slaughterPercentage: numberOrNull(draft.faenaPercent),
21428      quantity: numericTextOrNull(draft.cantidad),
21429      averageWeight: numberOrNull(draft.pesoProm),
21430      paymentTerm: String(draft.plazo || '').trim() || null,
21431      buyer: String(draft.comprador || '').trim() || null,
21432      divisions: draft.divisions?.length?window.RosganSaleDivisions.normalize(draft.divisions,draft.cantidad):[],
21433    };
21434  }
21435  async refreshCabanaTvCommercial() {
21436    if (this._cabanaTvCommercialFetching) return;
21437    const item = this.state.currentCabana;
21438    const auctionId = String(this.cabanaRemateId(item) || '');
21439    if (!auctionId) return;
21440    this._cabanaTvCommercialFetching = true;
21441    try {
21442      const existing = this.state.cabanaLots || [];
21443      const selectedKey = this.state.cabanaTvLotKey || (existing.length ? this.cabanaLotGroupKey(existing[0], 0) : '');
21444      const selected = existing.filter((lot, index) => this.cabanaLotGroupKey(lot, index) === selectedKey);
21445      if (!selected.length || this._cabanaTvCatalogAuction !== auctionId || Date.now() - (this._cabanaTvCatalogAt || 0) >= 30000) {
21446        await this.fetchCabanaCommercial(item, { silent: true, tvRefresh: true });
21447        this._cabanaTvCatalogAuction = auctionId;
21448        this._cabanaTvCatalogAt = Date.now();
21449        return;
21450      }
21451      const certs = [...new Set(selected.map(lot => String(lot.lotCert || lot.groupId || lot.id)))];
21452      const groups = await Promise.all(certs.map(cert => this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(cert), { cache: 'no-store' })));
21453      if (String(this.cabanaRemateId(this.state.currentCabana)) !== auctionId || !['cabanaTvLower', 'cabanaTvConfig'].includes(this.state.route)) return;
21454      const updated = this.normalizeCabanaAuctionLots(groups, item);
21455      const lots = this.cabanaSortLotsByCategoryOrder([
21456        ...(this.state.cabanaLots || []).filter(lot => !certs.includes(String(lot.lotCert || lot.groupId || lot.id))),
21457        ...updated,
21458      ]);
21459      const drafts = { ...(this.state.cabanaCommercialDrafts || {}) };
21460      existing.filter(lot => certs.includes(String(lot.lotCert || lot.groupId || lot.id))).forEac
21460h(lot => delete drafts[this.cabanaCommercialRowKey(lot)]);
21461      updated.forEach(lot => {
21462        if (lot.commercial) drafts[this.cabanaCommercialRowKey(lot)] = this.normalizeCabanaCommercialRecord(lot.commercial);
21463      });
21464      this._lastPriceBoardDraftsRaw = JSON.stringify(drafts);
21465      this.saveCabanaCommercialDrafts(item, drafts);
21466      this.setState({ cabanaLots: lots, cabanaCommercialDrafts: drafts });
21467    } catch {
21468      // A removed group or temporary failure requires a fresh catalog next time.
21469      this._cabanaTvCatalogAt = 0;
21470    } finally {
21471      this._cabanaTvCommercialFetching = false;
21472    }
21473  }
21474  canEditCabanaCommercialTerms(item = this.state.currentCabana) {
21475    const s=this.state,id=this.cabanaRemateId(item);
21476    return !!s.user?.uid && this.apiRoleIds(s.apiUser).some(r=>['admin','sysadmin','consignatario','cabanero'].includes(r))
21477      && s.cabanaPrebidSummaryAccess?.uid===s.user.uid && s.cabanaPrebidSummaryAccess?.id===id;
21478  }
21479  async loadCabanaCommercialTerms(item) {
21480    const id=this.cabanaRemateId(item); if(!id)return;
21481    try {
21482      const data=await this.cabanaApiFetch('/api/cabana-commercial-terms/'+encodeURIComponent(id),{cache:'no-store'});
21483      if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCommercialTerms:{id,...data}});
21484    } catch { if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCommercialTerms:{id,error:true}}); }
21485  }
21486  async openCabanaCommercialTerms() {
21487    const item=this.state.currentCabana,id=this.cabanaRemateId(item);
21488    if(!id)return;
21489    await this.loadCabanaCommercialTerms(item);
21490    if(this.cabanaRemateId(this.state.currentCabana)!==id)return;
21491    const data=this.state.cabanaCommercialTerms;
21492    if(data?.error){this.showToast('No se pudieron cargar las condiciones comerciales. Volvé a intentarlo.');return;}
21493    window.CabanaCommercialTerms.open({imageUrl:data?.imageUrl||'',canEdit:this.canEditCabanaCommercialTerms(item),
21494      title:item.nombre_remate||item.titulo||item.title||'Remate de cabaña',
21495      save:input=>this.cabanaApiFetch('/api/cabana-commercial-terms/'+encodeURIComponent(id),{method:'PUT',body:JSON.stringify(input)}),
21496      onSaved:result=>{if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCommercialTerms:{id,...result}});}
21497    });
21498  }
21499  async loadCabanaInstallments(item) {
21500    const id=this.cabanaRemateId(item);
21501    const config=await this.cabanaApiFetch('/api/cabana-installments/'+encodeURIComponent(id),{cache:'no-store'});
21502    item.installmentConfig=config;
21503    if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({currentCabana:{...this.state.currentCabana,installmentConfig:config}});
21504    return config;
21505  }
21506  async editCabanaInstallments(lot=null) {
21507    if(this._cabanaInstallmentBusy)return;
21508    const remate=this.state.currentCabana,id=this.cabanaRemateId(remate);
21509    const config=remate.installmentConfig||{},effective=window.CabanaInstallments.resolve(config,lot);
21510    const result=await window.CabanaPrebid.editDialog({title:lot?'Cuotas de este lote':'Preoferta por cuota',
21511      description:lot?'Se aplica a todos los animales de este lote. Podés volver a la cantidad general del remate.':'El monto ingresado será una cuota. El total se calcula multiplicándolo por la cantidad de cuotas. Las excepciones por lote se conservan. Se recalculan también los importes ya cargados.',
21512      fields:lot?[{key:'count',label:'Cantidad de cuotas',value:effective.count,min:1}]:[{key:'enabled',label:'Modalidad',value:config.enabled?'yes':'no',options:[{value:'no',label:'Importe total (sin multiplicar)'},{value:'yes',label:'Preofertar por cuota'}]},{key:'count',label:'Cuotas para todo el remate',value:config.count||12,min:1}],resetLabel:lot?'Usar configuración del remate':null});
21513    if(!result)return;
21514    if(!result.reset&&!window.CabanaInstallments.count(result.count)){this.showToast('Ingresá entre 1 y 120 cuotas enteras.');return;}
21515    this._cabanaInstallmentBusy=true;
21516    try{
21517      const patch=lot?{lotCert:this.cabanaPrebidCert(lot),override:result.reset?null:Number(result.count)}:{enabled:result.enabled==='yes',count:Number(result.count)};
21518      const saved=await this.cabanaApiFetch('/api/cabana-installments/'+encodeURIComponent(id),{method:'PATCH',body:JSON.stringify(patch)});
21519      if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({currentCabana:{...this.state.currentCabana,installmentConfig:saved}});
21520      this.showToast('Configuración de cuotas guardada.');
21521    }catch(e){this.showToast(e.message||'No se pudo guardar.');}finally{this._cabanaInstallmentBusy=false;}
21522  }
21523  async fetchCabanaCommercial(item = this.state.currentCabana, options = {}) {
21524    const auctionId = this.cabanaRemateId(item);
21525    if (!auctionId) return [];
21526    if (!options.silent) this.setState({ cabanaCommercialLoading: true });
21527    try {
21528      await this.loadCabanaInstallments(item);
21529      this.fetchCabanaCatalogPrices(item);
21530      const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/', { cache: 'no-store' });
21531      if (options.tvRefresh && (String(this.cabanaRemateId(this.state.currentCabana)) !== String(auctionId) || !['cabanaTvLower', 'cabanaTvConfig'].includes(this.state.route))) return [];
21532      const groups = this.listFrom(data, ['data', 'items', 'lots', 'lotes', 'groups']);
21533      const normalizedLots = this.normalizeCabanaAuctionLots(groups, item);
21534      const lots = normalizedLots;
21535      const remoteDrafts = {};
21536      lots.forEach((lot) => {
21537        if (!lot?.commercial) return;
21538        remoteDrafts[this.cabanaCommercialRowKey(lot)] = this.normalizeCabanaCommercialRecord(lot.commercial);
21539      });
21540      const raw = JSON.stringify(remoteDrafts);
21541      const changed = raw !== JSON.stringify(this.state.cabanaCommercialDrafts || {});
21542      this._lastPriceBoardDraftsRaw = raw;
21543      this.saveCabanaCommercialDrafts(item, remoteDrafts);
21544      this.setState({ cabanaLots: lots, currentCabana: item, cabanaLotsLoading: false, cabanaCommercialLoading: false, cabanaCommercialDrafts: remoteDrafts, ...(changed ?{ cabanaPriceBoardPulse: Date.now() } : {}) });
21545      return lots;
21546    } catch (error) {
21547      this.setState({ cabanaCommercialLoading: false, cabanaLotsLoading: false });
21548      if (!options.silent) this.showToast(error?.message || 'No se pudieron cargar los precios guardados.');
21549      return this.state.cabanaLots || [];
21550    }
21551  }
21552  openCabanaCommercial(item) {
21553    item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
21554    if (!item) return;
21555    if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item);
21556    if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/comercial') {
21557      this.writeAppHistory('/lacassina/comercial');
21558      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/comercial';
21559      window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina-commercial';
21560      document.documentElement.dataset.publicRoute = 'lacassina-commercial';
21561    } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'comercial');
21562    const hasLots = Array.isArray(this.state.cabanaLots);
21563    this.setState({
21564      route: 'cabanaCommercial',
21565      currentCabana: item,
21566      currentCabanaLot: null,
21567      currentCabanaLotVariants: [],
21568      cabanaLotsLoading: !hasLots,
21569      cabanaCommercialDrafts: this.loadCabanaCommercialDrafts(item),
21570      cabanaCommercialLoading: true,
21571      cabanaCommercialView: 'carga',
21572      cabanaCommercialOpenRow: '',
21573      cabanaPublicDeepLoading: false,
21574    });
21575    window.scrollTo(0, 0);
21576    this.fetchCabanaCommercial(item);
21577    this.ensureCabanaBrand(item);
21578  }
21579  openCabanaPriceBoard(item) {
21580    item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
21581    if (!item) return;
21582    if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item);
21583    if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/precios') {
21584      this.writeAppHistory('/lacassina/precios');
21585      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/precios';
21586      window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina-prices';
21587      document.documentElement.dataset.publicRoute = 'lacassina-prices';
21588    } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'precios');
21589    const hasLots = Array.isArray(this.state.cabanaLots);
21590    const drafts = this.loadCabanaCommercialDrafts(item);
21591    this._lastPriceBoardDraftsRaw = JSON.stringify(drafts || {});
21592    this.setState({
21593      route: 'cabanaPriceBoard',
21594      currentCabana: item,
21595      currentCabanaLot: null,
21596      currentCabanaLotVariants: [],
21597      cabanaLotsLoading: !hasLots,
21598      cabanaCommercialDrafts: drafts,
21599      cabanaPriceBoardPulse: Date.now(),
21600      cabanaPublicDeepLoading: false,
21601    });
21602    window.scrollTo(0, 0);
21603    this.fetchCabanaCommercial(item, { silent: true });
21604    this.ensureCabanaBrand(item);
21605  }
21606  openCabanaTvLower(item) {
21607    item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
21608    if (!item) return;
21609    if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item);
21610    if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/tv') {
21611      this.writeAppHistory('/lacassina/tv');
21612      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/tv';
21613      window.__ROSGAN_PUBLIC_ROUTE__ = 'cabana-tv';
21614      document.documentElement.dataset.publicRoute = 'cabana-tv';
21615    } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'tv');
21616    const hasLots = Array.isArray(this.state.cabanaLots);
21617    const storedKey = this.cabanaTvStorageKey();
21618    const selectedKey = storedKey ?(localStorage.getItem(storedKey) || '') : '';
21619    this.setState({
21620      route: 'cabanaTvLower',
21621      currentCabana: item,
21622      currentCabanaLot: null,
21623      currentCabanaLotVariants: [],
21624      cabanaLotsLoading: !hasLots,
21625      cabanaPublicDeepLoading: false,
21626      cabanaTvLotKey: selectedKey,
21627      cabanaTvConfig: this.loadCabanaTvConfig(),
21628      cabanaTvPulse: Date.now(),
21629      cabanaTvCycleStartedAt: Date.now(),
21630    });
21631    window.scrollTo(0, 0);
21632    if (!hasLots) this.fetchCabanaLots(item);
21633    this.ensureCabanaBrand(item);
21634    this.fetchCabanaCommercial(item, { silent: true });
21635    this.fetchCabanaTvRemoteConfig({ silent: true, force: true });
21636    this.fetchCabanaTvWeather(item, { force: true });
21637  }
21638  openCabanaTvConfig(item) {
21639    item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
21640    if (!item) return;
21641    if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item);
21642    this.destroyCabanaTvVideoMount();
21643    if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/tv/config') {
21644      this.writeAppHistory('/lacassina/tv/config');
21645      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/tv/config';
21646      window.__ROSGAN_PUBLIC_ROUTE__ = 'cabana-tv-config';
21647      document.documentElement.dataset.publicRoute = 'cabana-tv-config';
21648    } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'tv/config');
21649    const hasLots = Array.isArray(this.state.cabanaLots);
21650    let selectedKey = '';
21651    try { selectedKey = localStorage.getItem(this.cabanaTvStorageKey()) || ''; } catch (error) {}
21652    this.setState({
21653      route: 'cabanaTvConfig',
21654      currentCabana: item,
21655      currentCabanaLot: null,
21656      currentCabanaLotVariants: [],
21657      cabanaLotsLoading: !hasLots,
21658      cabanaPublicDeepLoading: false,
21659      cabanaTvLotKey: selectedKey,
21660      cabanaTvConfig: this.loadCabanaTvConfig(),
21661      cabanaTvPulse: Date.now(),
21662    });
21663    window.scrollTo(0, 0);
21664    if (!hasLots) this.fetchCabanaLots(item);
21665    this.ensureCabanaBrand(item);
21666    this.fetchCabanaCommercial(item, { silent: true });
21667    this.fetchCabanaTvRemoteConfig({ silent: true, force: true });
21668  }
21669  cabanaTvStorageKey() {
21670    const auctionId = this.cabanaRemateId(this.state.currentCabana) || 'lacassina';
21671    return 'rosgan:tvLower:' + auctionId + ':selectedLot';
21672  }
21673  cabanaTvConfigStorageKey() {
21674    const auctionId = this.cabanaRemateId(this.state.currentCabana) || 'lacassina';
21675    return 'rosgan:tvLower:' + auctionId + ':config';
21676  }
21677  cabanaTvDefaultFields() {
21678    return ['rp'];
21679  }
21680  defaultCabanaTvConfig() {
21681    return { visible: true, lowerThirdVisible: true, saleColumnVisible: true, contactsVisible: false, contacts: '', waitingVisible: false, waitingRestore: {}, endVisible: false, endRestore: {}, fields: this.cabanaTvDefaultFields(), fieldsByCategory: {}, depsEnabled: false, logosVisible: true, weatherVisible: true, clockVisible: true, kickerVisible: true, videoVisible: false, videoMode: 'single', videoRpKey: '', videoPlaying: true, videoRestartToken: 0 };
21682  }
21683  normalizeCabanaTvConfig(config, allowedFields = null, categoryKey = '') {
21684    const defaults = this.defaultCabanaTvConfig();
21685    const allowedList = Array.isArray(allowedFields) && allowedFields.length ?allowedFields : defaults.fields;
21686    const allowed = new Set(allowedList);
21687    const normalizeFields = (source, fallback = allowedList) => {
21688      const list = Array.isArray(source) ?source : fallback;
21689      const fields = list.filter((field) => allowed.has(field));
21690      return fields.length ?fields : [allowedList[0] || 'rp'];
21691    };
21692    const fields = normalizeFields(config?.fields, allowedList);
21693    const fieldsByCategory = {};
21694    if (config?.fieldsByCategory && typeof config.fieldsByCategory === 'object') {
21695      Object.entries(config.fieldsByCategory).forEach(([key, value]) => {
21696        if (!key || !Array.isArray(value)) return;
21697        fieldsByCategory[key] = value.filter(Boolean);
21698      });
21699    }
21700    if (categoryKey) fieldsByCategory[categoryKey] = normalizeFields(fieldsByCategory[categoryKey] || fields, allowedList);
21701    const waitingElementKeys = ['lowerThirdVisible', 'saleColumnVisible', 'contactsVisible', 'logosVisible', 'weatherVisible', 'clockVisible', 'kickerVisible', 'videoVisible'];
21702    const waitingRestore = {};
21703    if (config?.waitingRestore && typeof config.waitingRestore === 'object') {
21704      waitingElementKeys.forEach((key) => {
21705        if (typeof config.waitingRestore[key] === 'boolean') waitingRestore[key] = config.waitingRestore[key];
21706      });
21707    }
21708    const endRestore = {};
21709    if (config?.endRestore && typeof config.endRestore === 'object') {
21710      waitingElementKeys.forEach((key) => {
21711        if (typeof config.endRestore[key] === 'boolean') endRestore[key] = config.endRestore[key];
21712      });
21713    }
21714    const endVisible = config?.endVisible === true;
21715    const waitingVisible = !endVisible && config?.waitingVisible === true;
21716    return {
21717      visible: config?.visible !== false,
21718      lowerThirdVisible: config?.lowerThirdVisible !== false,
21719      saleColumnVisible: config?.saleColumnVisible !== false,
21720      contactsVisible: config?.contactsVisible === true,
21721      contacts: String(config?.contacts || '').slice(0, 2000),
21722      waitingVisible,
21723      waitingRestore,
21724      endVisible,
21725      endRestore,
21726      fields,
21727      fieldsByCategory,
21728      depsEnabled: config?.depsEnabled === true,
21729      logosVisible: config?.logosVisible !== false,
21730      weatherVisible: config?.weatherVisible !== false,
21731      clockVisible: config?.clockVisible !== false,
21732      kickerVisible: config?.kickerVisible !== false,
21733      videoVisible: config?.videoVisible === true,
21734      videoMode: ['single', 'grid'].includes(config?.videoMode) ?config.videoMode : 'single',
21735      videoRpKey: String(config?.videoRpKey || '').trim(),
21736      videoPlaying: config?.videoPlaying !== false,
21737      videoRestartToken: Number.isFinite(Number(config?.videoRestartToken)) ?Number(config.videoRestartToken) : 0,
21738    };
21739  }
21740  cabanaTvFieldsForCategory(config, categoryKey, allowedFields = null) {
21741    const normalized = this.normalizeCabanaTvConfig(config, allowedFields, categoryKey);
21742    return normalized.fieldsByCategory?.[categoryKey] || normalized.fields;
21743  }
21744  loadCabanaTvConfig() {
21745    try {
21746      const raw = localStorage.getItem(this.cabanaTvConfigStorageKey());
21747      if (raw) return this.normalizeCabanaTvConfig(JSON.parse(raw));
21748    } catch (error) {}
21749    return this.defaultCabanaTvConfig();
21750  }
21751  saveCabanaTvConfig(patch = {}) {
21752    const allowed = this._cabanaTvFieldKeys?.length ?this._cabanaTvFieldKeys : null;
21753    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), allowed);
21754    const next = this.normalizeCabanaTvConfig({ ...current, ...patch }, allowed);
21755    this._cabanaTvLocalConfigUpdatedAt = Date.now();
21756    try { localStorage.setItem(this.cabanaTvConfigStorageKey(), JSON.stringify(next)); } catch (error) {}
21757    this.setState((previous) =>
21757 ({ cabanaTvConfig: next, cabanaTvConfigPulse: Number(previous.cabanaTvConfigPulse || 0) + 1 }));
21758    try { window.dispatchEvent(new CustomEvent('rosgan:tv-config')); } catch (error) {}
21759    this.queueCabanaTvRemoteSave({ config: next });
21760  }
21761  toggleCabanaTvVisible() {
21762    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21763    this.saveCabanaTvConfig({ visible: !current.visible });
21764  }
21765  toggleCabanaTvDeps() {
21766    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21767    this.saveCabanaTvConfig({ depsEnabled: !current.depsEnabled });
21768  }
21769  toggleCabanaTvElement(key) {
21770    const cleanKey = String(key || '');
21771    const waitingElementKeys = ['lowerThirdVisible', 'saleColumnVisible', 'contactsVisible', 'logosVisible', 'weatherVisible', 'clockVisible', 'kickerVisible', 'videoVisible'];
21772    const plateKeys = ['waitingVisible', 'endVisible'];
21773    if (![...waitingElementKeys, ...plateKeys].includes(cleanKey)) return;
21774    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21775    if (plateKeys.includes(cleanKey)) {
21776      const activePlateKey = current.endVisible === true ?'endVisible' : (current.waitingVisible === true ?'waitingVisible' : '');
21777      const restoreKey = cleanKey === 'endVisible' ?'endRestore' : 'waitingRestore';
21778      const memoryKey = cleanKey === 'endVisible' ?'_cabanaTvEndRestore' : '_cabanaTvWaitingRestore';
21779      if (current[cleanKey] !== true) {
21780        const activeRestoreKey = activePlateKey === 'endVisible' ?'endRestore' : 'waitingRestore';
21781        const activeMemoryKey = activePlateKey === 'endVisible' ?'_cabanaTvEndRestore' : '_cabanaTvWaitingRestore';
21782        const activeRestore = activePlateKey ?(Object.keys(current[activeRestoreKey] || {}).length ?current[activeRestoreKey] : (this[activeMemoryKey] || {})) : {};
21783        const waitingRestore = activePlateKey
21784          ?Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, typeof activeRestore[elementKey] === 'boolean' ?activeRestore[elementKey] : this.defaultCabanaTvConfig()[elementKey]]))
21785          : Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, current[elementKey] !== false]));
21786        const disabledElements = Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, false]));
21787        this[memoryKey] = waitingRestore;
21788        if (activePlateKey && activeMemoryKey !== memoryKey) this[activeMemoryKey] = null;
21789        this.setState({ cabanaTvVideoIndex: 0 });
21790        this.saveCabanaTvConfig({
21791          ...disabledElements,
21792          waitingVisible: cleanKey === 'waitingVisible',
21793          waitingRestore: cleanKey === 'waitingVisible' ?waitingRestore : {},
21794          endVisible: cleanKey === 'endVisible',
21795          endRestore: cleanKey === 'endVisible' ?waitingRestore : {},
21796        });
21797        return;
21798      }
21799      const restoreSource = Object.keys(current[restoreKey] || {}).length ?current[restoreKey] : (this[memoryKey] || {});
21800      const restoredElements = Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, typeof restoreSource[elementKey] === 'boolean' ?restoreSource[elementKey] : this.defaultCabanaTvConfig()[elementKey]]));
21801      this._cabanaTvWaitingRestore = null;
21802      this._cabanaTvEndRestore = null;
21803      this.setState({ cabanaTvVideoIndex: 0 });
21804      this.saveCabanaTvConfig({ ...restoredElements, waitingVisible: false, waitingRestore: {}, endVisible: false, endRestore: {} });
21805      return;
21806    }
21807    if (current.waitingVisible === true || current.endVisible === true) return;
21808    if (cleanKey === 'videoVisible') {
21809      const becomingVisible = current.videoVisible !== true;
21810      this.setState({ cabanaTvVideoIndex: 0 });
21811      this.saveCabanaTvConfig({
21812        videoVisible: becomingVisible,
21813        ...(becomingVisible ?{ videoPlaying: true, videoRestartToken: Number(current.videoRestartToken || 0) + 1 } : {}),
21814      });
21815      return;
21816    }
21817    this.saveCabanaTvConfig({ [cleanKey]: current[cleanKey] === false });
21818  }
21819  setCabanaTvVideoMode(mode) {
21820    const cleanMode = String(mode || '') === 'grid' ?'grid' : 'single';
21821    this.setState({ cabanaTvVideoIndex: 0 });
21822    this.saveCabanaTvConfig({ videoMode: cleanMode });
21823  }
21824  setCabanaTvVideoRpKey(key) {
21825    const cleanKey = String(key || '').trim();
21826    if (!cleanKey) return;
21827    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21828    this.saveCabanaTvConfig({ videoRpKey: cleanKey, videoPlaying: true, videoRestartToken: Number(current.videoRestartToken || 0) + 1 });
21829  }
21830  stepCabanaTvVideoRp(items = [], direction = 1) {
21831    const ready = Array.isArray(items) ?items.filter((item) => item?.src) : [];
21832    if (!ready.length) return;
21833    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21834    const currentIndex = Math.max(0, ready.findIndex((item) => item.key === current.videoRpKey));
21835    const next = ready[((currentIndex + Number(direction || 1)) % ready.length + ready.length) % ready.length];
21836    if (next) this.setCabanaTvVideoRpKey(next.key);
21837  }
21838  toggleCabanaTvVideoPlayback(fallbackKey = '') {
21839    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21840    this.saveCabanaTvConfig({ videoRpKey: current.videoRpKey || String(fallbackKey || ''), videoPlaying: !current.videoPlaying });
21841  }
21842  restartCabanaTvVideo(fallbackKey = '') {
21843    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21844    this.saveCabanaTvConfig({ videoRpKey: current.videoRpKey || String(fallbackKey || ''), videoPlaying: true, videoRestartToken: Number(current.videoRestartToken || 0) + 1 });
21845  }
21846  handleCabanaTvVideoEnded(event) {
21847    const readyCount = Number(this._cabanaTvReadyVideoCount || 0);
21848    if (readyCount <= 1 && event?.target) {
21849      try {
21850        const video = event.target;
21851        video.currentTime = 0;
21852        const playResult = video.play?.();
21853        if (playResult?.catch) playResult.catch(() => {});
21854      } catch (error) {}
21855      return;
21856    }
21857    this.setState((previous) => ({ cabanaTvVideoIndex: Number(previous.cabanaTvVideoIndex || 0) + 1 }));
21858  }
21859  restartCabanaTvLoopVideo(event) {
21860    const video = event?.target;
21861    if (!video) return;
21862    try {
21863      video.currentTime = 0;
21864      const playResult = video.play?.();
21865      if (playResult?.catch) playResult.catch(() => {});
21866    } catch (error) {}
21867  }
21868  advanceCabanaTvPlaylist(event) {
21869    const currentVideo = event?.target;
21870    const currentTile = currentVideo?.closest?.('.tv-video-tile');
21871    const mount = currentTile?.parentElement;
21872    const tiles = mount ?Array.from(mount.querySelectorAll('.tv-video-tile')) : [];
21873    if (!currentVideo || !currentTile || tiles.length <= 1) {
21874      this.handleCabanaTvVideoEnded(event);
21875      return;
21876    }
21877    if (currentTile.dataset.advancing === 'true') return;
21878    currentTile.dataset.advancing = 'true';
21879    const currentIndex = Math.max(0, tiles.indexOf(currentTile));
21880    const nextTile = tiles[(currentIndex + 1) % tiles.length];
21881    const nextVideo = nextTile?.querySelector('video');
21882    if (!nextVideo) {
21883      currentTile.dataset.advancing = '';
21884      this.handleCabanaTvVideoEnded(event);
21885      return;
21886    }
21887    let activated = false;
21888    const activateNext = () => {
21889      if (activated) return;
21890      activated = true;
21891      nextVideo.classList.add('is-ready');
21892      nextTile.classList.add('is-active');
21893      currentTile.classList.remove('is-active');
21894      const transitionMs = mount.classList.contains('is-waiting-background') ?1050 : 300;
21895      window.setTimeout(() => {
21896        try { currentVideo.pause(); } catch (error) {}
21897        try { currentVideo.currentTime = 0; } catch (error) {}
21898        currentTile.dataset.advancing = '';
21899      }, transitionMs);
21900    };
21901    nextVideo.addEventListener('playing', activateNext, { once: true });
21902    try {
21903      nextVideo.currentTime = 0;
21904      const playResult = nextVideo.play?.();
21905      if (playResult?.catch) playResult.catch(() => {});
21906      if (nextVideo.readyState >= 3 && !nextVideo.paused) activateNext();
21907    } catch (error) {
21908      currentTile.dataset.advancing = '';
21909      try {
21910        currentVideo.currentTime = 0;
21911        currentVideo.play?.();
21912      } catch (_) {}
21913    }
21914  }
21915  toggleCabanaTvField(fieldKey, categoryKeyOverride = '') {
21916    const key = String(fieldKey || '');
21917    if (!key) return;
21918    const categoryKey = String(categoryKeyOverride || this._cabanaTvActiveCategoryKey || '');
21919    const allowed = (categoryKey && this._cabanaTvFieldKeysByCategory?.[categoryKey]?.length) ?this._cabanaTvFieldKeysByCategory[categoryKey] : (this._cabanaTvFieldKeys?.length ?this._cabanaTvFieldKeys : this.cabanaTvDefaultFields());
21920    const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), allowed, categoryKey);
21921    const set = new Set(this.cabanaTvFieldsForCategory(current, categoryKey, allowed));
21922    if (set.has(key) && set.size > 1) set.delete(key);
21923    else set.add(key);
21924    const next = {
21925      ...current,
21926      fieldsByCategory: {
21927        ...(current.fieldsByCategory || {}),
21928        [categoryKey]: allowed.filter((field) => set.has(field)),
21929      },
21930    };
21931    this._cabanaTvLocalConfigUpdatedAt = Date.now();
21932    try { localStorage.setItem(this.cabanaTvConfigStorageKey(), JSON.stringify(next)); } catch (error) {}
21933    this.setState((previous) =>
21933 ({ cabanaTvConfig: next, cabanaTvConfigPulse: Number(previous.cabanaTvConfigPulse || 0) + 1 }));
21934    try { window.dispatchEvent(new CustomEvent('rosgan:tv-config')); } catch (error) {}
21935    this.queueCabanaTvRemoteSave({ config: next });
21936  }
21937  cabanaTvRemotePath() {
21938    const auctionId = this.cabanaRemateId(this.state.currentCabana) || this.cabanaRemateId(this.state.cabanas?.[0]) || '2';
21939    return '/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/tv-config';
21940  }
21941  normalizeCabanaTvRemotePayload(data) {
21942    const root = data?.data || data?.item || data || {};
21943    return {
21944      selectedLotKey: String(root.selectedLotKey || root.lotKey || root.selected_lot_key || '').trim(),
21945      config: this.normalizeCabanaTvConfig(root.config || root),
21946      updatedAt: root.updatedAt || root.updated_at || '',
21947    };
21948  }
21949  async fetchCabanaTvRemoteConfig(options = {}) {
21950    if (this.state.route !== 'cabanaTvLower' && this.state.route !== 'cabanaTvConfig') return null;
21951    if (!this.state.currentCabana) return null;
21952    const now = Date.now();
21953    if (!options.force && this._lastCabanaTvRemoteFetchAt && now - this._lastCabanaTvRemoteFetchAt < 700) return null;
21954    if (!options.force && this._cabanaTvLocalConfigUpdatedAt && now - this._cabanaTvLocalConfigUpdatedAt < 1600) return null;
21955    this._lastCabanaTvRemoteFetchAt = now;
21956    try {
21957      const data = await this.cabanaApiFetch(this.cabanaTvRemotePath(), { cache: 'no-store' });
21958      const remote = this.normalizeCabanaTvRemotePayload(data);
21959      const signature = JSON.stringify(remote);
21960      if (signature && signature !== this._lastCabanaTvRemoteSignature) {
21961        this._lastCabanaTvRemoteSignature = signature;
21962        try {
21963          if (remote.selectedLotKey) localStorage.setItem(this.cabanaTvStorageKey(), remote.selectedLotKey);
21964          localStorage.setItem(this.cabanaTvConfigStorageKey(), JSON.stringify(remote.config));
21965        } catch (error) {}
21966        this.setState((previous) => ({
21967          cabanaTvLotKey: remote.selectedLotKey || previous.cabanaTvLotKey,
21968          cabanaTvConfig: remote.config,
21969          cabanaTvPulse: remote.selectedLotKey && remote.selectedLotKey !== previous.cabanaTvLotKey ?Number(previous.cabanaTvPulse || 0) + 1 : previous.cabanaTvPulse,
21970          cabanaTvConfigPulse: Number(previous.cabanaTvConfigPulse || 0) + 1,
21971          cabanaTvCycleStartedAt: (remote.selectedLotKey && remote.selectedLotKey !== previous.cabanaTvLotKey) ?Date.now() : previous.cabanaTvCycleStartedAt,
21972          cabanaTvVideoIndex: (remote.selectedLotKey && remote.selectedLotKey !== previous.cabanaTvLotKey) ?0 : previous.cabanaTvVideoIndex,
21973        }));
21974      }
21975      return remote;
21976    } catch (error) {
21977      if (!options.silent && error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo sincronizar el zocalo.');
21978      return null;
21979    }
21980  }
21981  queueCabanaTvRemoteSave(patch = {}) {
21982    clearTimeout(this._cabanaTvRemoteSaveTimer);
21983    this._cabanaTvRemoteSaveTimer = setTimeout(() => this.saveCabanaTvRemoteConfig(patch), 220);
21984  }
21985  async saveCabanaTvRemoteConfig(patch = {}) {
21986    if (!this.state.currentCabana) return;
21987    const rawConfig = patch.config || this.state.cabanaTvConfig || this.loadCabanaTvConfig();
21988    const payload = {
21989      selectedLotKey: patch.selectedLotKey || this.state.cabanaTvLotKey || '',
21990      config: this.normalizeCabanaTvConfig(rawConfig),
21991    };
21992    try {
21993      const data = await this.cabanaApiFetch(this.cabanaTvRemotePath(), { method: 'PUT', body: JSON.stringify(payload) });
21994      this._lastCabanaTvRemoteSignature = JSON.stringify(this.normalizeCabanaTvRemotePayload(data || payload));
21995    } catch (error) {
21996      if (error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo guardar la config del zocalo.');
21997    }
21998  }
21999  syncCabanaTvExternalState() {
22000    if (this.state.route !== 'cabanaTvLower' && this.state.route !== 'cabanaTvConfig') return;
22001    let selectedKey = '';
22002    try { selectedKey = localStorage.getItem(this.cabanaTvStorageKey()) || ''; } catch (error) {}
22003    const nextConfig = this.loadCabanaTvConfig();
22004    const currentConfig = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig());
22005    const configChanged = JSON.stringify(nextConfig) !== JSON.stringify(currentConfig);
22006    const lotChanged = selectedKey && selectedKey !== this.state.cabanaTvLotKey;
22007    if (lotChanged || configChanged) {
22008      this.setState((previous) => ({
22009        cabanaTvLotKey: lotChanged ?selectedKey : previous.cabanaTvLotKey,
22010        cabanaTvConfig: configChanged ?nextConfig : previous.cabanaTvConfig,
22011        cabanaTvPulse: lotChanged ?Number(previous.cabanaTvPulse || 0) + 1 : previous.cabanaTvPulse,
22012        cabanaTvConfigPulse: configChanged ?Number(previous.cabanaTvConfigPulse || 0) + 1 : previous.cabanaTvConfigPulse,
22013        cabanaTvCycleStartedAt: (lotChanged || configChanged) ?Date.now() : previous.cabanaTvCycleStartedAt,
22014        cabanaTvVideoIndex: (lotChanged || configChanged) ?0 : previous.cabanaTvVideoIndex,
22015      }));
22016    }
22017  }
22018  setCabanaTvLotKey(key) {
22019    const nextKey = String(key || '');
22020    if (!nextKey) return;
22021    try { localStorage.setItem(this.cabanaTvStorageKey(), nextKey); } catch (error) {}
22022    this.setState((previous) => ({ cabanaTvLotKey: nextKey, cabanaTvPulse: Number(previous.cabanaTvPulse || 0) + 1, cabanaTvCycleStartedAt: Date.now(), cabanaTvVideoIndex: 0 }));
22023    try { window.dispatchEvent(new CustomEvent('rosgan:tv-config')); } catch (error) {}
22024    this.queueCabanaTvRemoteSave({ selectedLotKey: nextKey });
22025  }
22026  refreshCabanaPriceBoardDrafts() {
22027    if (this.state.route !== 'cabanaPriceBoard') return;
22028    this.fetchCabanaCommercial(this.state.currentCabana, { silent: true });
22029  }
22030  consumeCabanaCommercialPendingPrices(lot) {
22031    const key = this.cabanaCommercialRowKey(lot);
22032    if (!key) return {};
22033    const patch = {};
22034    ['kilo', 'cabeza'].forEach((priceMode) => {
22035      const timerKey = key + ':' + priceMode;
22036      if (!Object.prototype.hasOwnProperty.call(this._cabanaCommercialPriceDrafts || {}, timerKey)) return;
22037      const raw = String(this._cabanaCommercialPriceDrafts[timerKey] ?? '');
22038      if (priceMode === 'kilo') patch.precioKilo = raw;
22039      else patch.precioCabeza = raw;
22040      patch.priceMode = priceMode;
22041      if (this._cabanaCommercialPriceInputTimers?.[timerKey]) {
22042        clearTimeout(this._cabanaCommercialPriceInputTimers[timerKey]);
22043        delete this._cabanaCommercialPriceInputTimers[timerKey];
22044      }
22045      delete this._cabanaCommercialPriceDrafts[timerKey];
22046    });
22047    return patch;
22048  }
22049  openCabanaSaleDivisions(lot, quantity, lotNumber) {
22050    const key=this.cabanaCommercialRowKey(lot),sale={...(this.state.cabanaCommercialDrafts[key]||{}),...this.consumeCabanaCommercialPendingPrices(lot)};
22051    if (!(Number(quantity)>0)) { this.showToast('Indicá primero la cantidad de animales.'); return; }
22052    window.openRosganSaleDivisions({lot:{cantidad:Number(quantity),lotNumber,cert:lot.itemId||lot.id},sale,mode:sale.priceMode||'cabeza',onSave:async(divisions,priceMode)=>{
22053      const auctionId=this.cabanaRemateId(this.state.currentCabana),lotCert=String(lot.lotCert||lot.groupId||this.cabanaLotId(lot));
22054      const latest=await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(auctionId)+'/lots/'+encodeURIComponent(lotCert),{cache:'no-store'});
22055      if(!latest?.saleDivisionsSupported)throw new Error('La API necesita actualizarse para guardar ventas divididas. No se modificó la venta.');
22056      const sold=divisions.filter(p=>p.price>0),soldQuantity=sold.reduce((n,p)=>n+p.quantity,0),average=soldQuantity?sold.reduce((n,p)=>n+p.price*p.quantity,0)/soldQuantity:0;
22057      this.updateCabanaCommercialRow(lot,{divisions,cantidad:String(quantity),priceMode,precioKilo:priceMode==='kilo'?String(Math.round(average)):'',precioCabeza:priceMode==='cabeza'?String(Math.round(average)):'',comprador:'',status:soldQuantity===Number(quantity)?'vendido':'pendiente'});
22058    }});
22059  }
22060  updateCabanaCommercialRow(lot, patch = {}) {
22061    const key = this.cabanaCommercialRowKey(lot);
22062    if (!key) return;
22063    const drafts = { ...(this.state.cabanaCommercialDrafts || {}) };
22064    const pendingPrices = this.consumeCabanaCommercialPendingPrices(lot);
22065    drafts[key] = { ...(drafts[key] || {}), ...pendingPrices, ...patch, updatedAt: new Date().toISOString() };
22066    this.setState({ cabanaCommercialDrafts: drafts }, () => {
22067      this.saveCabanaCommercialDrafts(this.state.currentCabana, drafts);
22068      this._cabanaCommercialPatchTimers = this._cabanaCommercialPatchTimers || {};
22069      clearTimeout(this._cabanaCommercialPatchTimers[key]);
22070      this._cabanaCommercialPatchTimers[key] = setTimeout(() => {
22071        delete this._cabanaCommercialPatchTimers[key];
22072        this.queueCabanaCommercialRemoteSave(lot, drafts[key]);
22073      }, 650);
22074    });
22075  }
22076  queueCabanaCommercialPriceInput(lot, priceMode, raw) {
22077    const key = this.cabanaCommercialRowKey(lot);
22078    if (!key) return;
22079    const timerKey = key + ':' + priceMode;
22080    this._cabanaCommercialPriceDrafts = this._cabanaCommercialPriceDrafts || {};
22081    this._cabanaCommercialPriceInputTimers = this._cabanaCommercialPriceInputTimers || {};
22082    this._cabanaCommercialPriceDrafts[timerKey] = String(raw ?? '');
22083    clearTimeout(this._cabanaCommercialPriceInputTimers[timerKey]);
22084    this._cabanaCommercialPriceInputTimers[timerKey] = setTimeout(() => {
22085      this.flushCabanaCommercialPriceInput(lot, priceMode);
22086    }, 500);
22087  }
22088  flushCabanaCommercialPriceInput(lot, priceMode, raw, patch = {}) {
22089    const key = this.cabanaCommercialRowKey(lot);
22090    if (!key) return;
22091    const timerKey = key + ':' + priceMode;
22092    const buffered = this._cabanaCommercialPriceDrafts?.[timerKey];
22093    const nextRaw = raw === undefined ?buffered : String(raw ?? '');
22094    if (nextRaw === undefined && !Object.keys(patch).length) return;
22095    if (this._cabanaCommercialPriceInputTimers?.[timerKey]) {
22096      clearTimeout(this._cabanaCommercialPriceInputTimers[timerKey]);
22097      delete this._cabanaCommercialPriceInputTimers[timerKey];
22098    }
22099    if (this._cabanaCommercialPriceDrafts) delete this._cabanaCommercialPriceDrafts[timerKey];
22100    const nextDraft = {
22101      ...((this.state.cabanaCommercialDrafts || {})[key] || {}),
22102      ...(priceMode === 'kilo' ?{ precioKilo: nextRaw ?? '', priceMode: 'kilo' } : { precioCabeza: nextRaw ?? '', priceMode: 'cabeza' }),
22103      ...patch,
22104      updatedAt: new Date().toISOString(),
22105    };
22106    const drafts = { ...(this.state.cabanaCommercialDrafts || {}), [key]: nextDraft };
22107    this.state.cabanaCommercialDrafts = drafts;
22108    this.saveCabanaCommercialDrafts(this.state.currentCabana, drafts);
22109    this.queueCabanaCommercialRemoteSave(lot, nextDraft);
22110    if (Object.keys(patch).length || this.state.currentCabana?.installmentConfig?.enabled) this.setState({ cabanaCommercialDrafts: drafts });
22111  }
22112  queueCabanaCommercialRemoteSave(lot, draft) {
22113    const key = this.cabanaCommercialRowKey(lot);
22114    if (!key || !draft) return;
22115    this._cabanaCommercialRemoteDrafts = this._cabanaCommercialRemoteDrafts || {};
22116    this._cabanaCommercialRemoteSaving = this._cabanaCommercialRemoteSaving || {};
22117    this._cabanaCommercialRemoteDrafts[key] = { lot, draft };
22118    if (this._cabanaCommercialRemoteSaving[key]) return;
22119    this._cabanaCommercialRemoteSaving[key] = true;
22120    const drain = async () => {
22121      try {
22122        while (this._cabanaCommercialRemoteDrafts[key]) {
22123          const pending = this._cabanaCommercialRemoteDrafts[key];
22124          delete this._cabanaCommercialRemoteDrafts[key];
22125          await this.saveCabanaCommercialRow(pending.lot, pending.draft);
22126        }
22127      } finally {
22128        delete this._cabanaCommercialRemoteSaving[key];
22129        if (this._cabanaCommercialRemoteDrafts[key]) this.queueCabanaCommercialRemoteSave(this._cabanaCommercialRemoteDrafts[key].lot, this._cabanaCommercialRemoteDrafts[key].draft);
22130      }
22131    };
22132    drain();
22133  }
22134  async saveCabanaCommercialRow(lot, draftOverride = null) {
22135    const auctionId = this.cabanaRemateId(this.state.currentCabana);
22136    const key = this.cabanaCommercialRowKey(lot);
22137    const draft = draftOverride || (this.state.cabanaCommercialDrafts || {})[key];
22138    const lotCert = String(lot?.lotCert || lot?.groupId || this.cabanaLotId(lot) || '');
22139    const itemId = String(lot?.itemId || lot?.id || '');
22140    if (!auctionId || !lotCert || !itemId || !draft) return;
22141    try {
22142      const payload = this.cabanaCommercialPayload(lot, draft);
22143      delete payload.lotCert;
22144      delete payload.itemId;
22145      await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/items/' + encodeURIComponent(itemId) + '/commercial', { method: 'PATCH', body: JSON.stringify(payload) });
22146    } catch (error) {
22147      this.showToast(error?.message || 'No se pudo guardar el cambio.');
22148    }
22149  }
22150  setCabanaCommercialFilter(filter) {
22151    this.setState({ cabanaCommercialFilter: filter || 'todos' });
22152  }
22153  toggleCabanaCommercialMore(lot) {
22154    const key = this.cabanaCommercialRowKey(lot);
22155    this.setState({ cabanaCommercialOpenRow: this.state.cabanaCommercialOpenRow === key ?'' : key });
22156  }
22157  toggleCabanaSalesOrderGrouped() {
22158    const grouped = !this.state.cabanaSalesOrderGrouped;
22159    let lots = [...(this.state.cabanaSalesOrderLots || [])];
22160    if (grouped) {
22161      const order = [];
22162      const buckets = new Map();
22163      lots.forEach((lot) => {
22164        const key = this.cabanaSalesCategoryKey(lot);
22165        if (!buckets.has(key)) { buckets.set(key, []); order.push(key); }
22166        buckets.get(key).push(lot);
22167      });
22168      lots = order.flatMap((key) => buckets.get(key));
22169    }
22170    this.setState({ cabanaSalesOrderGrouped: grouped, cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(lots), cabanaSalesOrderError: '', cabanaSalesOrderSaved: false });
22171  }
22172  moveCabanaSalesLot(sourceCert, targetCert) {
22173    const lots = [...(this.state.cabanaSalesOrderLots || [])];
22174    const sourceIndex = lots.findIndex((lot) => this.cabanaLotId(lot) === String(sourceCert));
22175    const targetIndex = lots.findIndex((lot) => this.cabanaLotId(lot) === String(targetCert));
22176    if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) return;
22177    if (this.state.cabanaSalesOrderGrouped && this.cabanaSalesCategoryKey(lots[sourceIndex]) !== this.cabanaSalesCategoryKey(lots[targetIndex])) return;
22178    const [moved] = lots.splice(sourceIndex, 1);
22179    lots.splice(targetIndex, 0, moved);
22180    this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(lots), cabanaSalesOrderError: '' });
22181  }
22182  moveCabanaSalesCategory(sourceKey, targetKey) {
22183    if (!sourceKey || !targetKey || sourceKey === targetKey) return;
22184    const lots = [...(this.state.cabanaSalesOrderLots || [])];
22185    const categoryOrder = [];
22186    const buckets = new Map();
22187    lots.forEach((lot) => {
22188      const key = this.cabanaSalesCategoryKey(lot);
22189      if (!buckets.has(key)) { buckets.set(key, []); categoryOrder.push(key); }
22190      buckets.get(key).push(lot);
22191    });
22192    const sourceIndex = categoryOrder.indexOf(sourceKey);
22193    const targetIndex = categoryOrder.indexOf(targetKey);
22194    if (sourceIndex < 0 || targetIndex < 0) return;
22195    const [moved] = categoryOrder.splice(sourceIndex, 1);
22196    categoryOrder.splice(targetIndex, 0, moved);
22197    this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(categoryOrder.flatMap((key) => buckets.get(key))), cabanaSalesOrderError: '' });
22198  }
22199  setCabanaSalesLotPosition(cert, requestedPosition) {
22200    const lots = [...(this.state.cabanaSalesOrderLots || [])];
22201    const sourceIndex = lots.findIndex((lot) => this.cabanaLotId(lot) === String(cert));
22202    if (sourceIndex < 0) return;
22203    const scopedIndices = this.state.cabanaSalesOrderGrouped
22204      ? lots.map((lot, index) => this.cabanaSalesCategoryKey(lot) === this.cabanaSalesCategoryKey(lots[sourceIndex]) ?index : -1).filter((index) => index >= 0)
22205      : lots.map((_, index) => index);
22206    const currentScopeIndex = scopedIndices.indexOf(sourceIndex);
22207    const targetScopeIndex = Math.max(0, Math.min(scopedIndices.length - 1, Number(requestedPosition) - 1));
22208    if (currentScopeIndex === targetScopeIndex) return;
22209    const targetIndex = scopedIndices[targetScopeIndex];
22210    const [moved] = lots.splice(sourceIndex, 1);
22211    lots.splice(targetIndex, 0, moved);
22212    this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(lots), cabanaSalesOrderError: '', cabanaSalesOrderSaved: false });
22213  }
22214  setCabanaSalesCategoryPosition(key, requestedPosition) {
22215    const lots = [...(this.state.cabanaSalesOrderLots || [])];
22216    const order = [];
22217    const buckets = new Map();
22218    lots.forEach((lot) => {
22219      const categoryKey = this.cabanaSalesCategoryKey(lot);
22220      if (!buckets.has(categoryKey)) { buckets.set(categoryKey, []); order.push(categoryKey); }
22221      buckets.get(categoryKey).push(lot);
22222    });
22223    const sourceIndex = order.indexOf(String(key));
22224    const targetIndex = Math.max(0, Math.min(order.length - 1, Number(requestedPosition) - 1));
22225    if (sourceIndex < 0 || sourceIndex === targetIndex) return;
22226    const [moved] = order.splice(sourceIndex, 1);
22227    order.splice(targetIndex, 0, moved);
22228    this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(order.flatMap((categoryKey) => buckets.get(categoryKey))), cabanaSalesOrderError: '', cabanaSalesOrderSaved: false });
22229  }
22230  destroyCabanaSalesOrderSorting() {
22231    if (this._cabanaSalesOrderBindTimer) clearTimeout(this._cabanaSalesOrderBindTimer);
22232    this._cabanaSalesOrderBindTimer = null;
22233    if (this._cabanaSalesOrderCleanup) {
22234      try { this._cabanaSalesOrderCleanup(); } catch (_) {}
22235    }
22236    this._cabanaSalesOrderCleanup = null;
22237  }
22238  bindCabanaSalesOrderSorting(attempt = 0) {
22239    if (attempt === 0) this.destroyCabanaSalesOrderSorting();
22240    if (this.state.route !== 'cabanaSalesOrder') return;
22241    requestAnimationFrame(() => {
22242      if (this.state.route !== 'cabanaSalesOrder') return;
22243      const root = document.querySelector('.sales-order-page');
22244      const hasLots = (this.state.cabanaSalesOrderLots || []).length > 0;
22245      const expectedList = root?.querySelector(
22246        this.state.cabanaSalesOrderGrouped ? '[data-sales-groups]' : '[data-sales-flat]'
22247      );
22248      if (!root || (hasLots && !expectedList)) {
22249        if (attempt < 30) {
22250          this._cabanaSalesOrderBindTimer = setTimeout(() => {
22251            this._cabanaSalesOrderBindTimer = null;
22252            this.bindCabanaSalesOrderSorting(attempt + 1);
22253          }, 60);
22254        } else {
22255          this.setState({ cabanaSalesOrderError: 'No se pudo activar el ordenamiento. Recargá esta pantalla.' });
22256        }
22257        return;
22258      }
22259      const certOfNode = (node) => String(node?.querySelector?.('.sales-order-cert')?.value || '');
22260      const categoryKeyOfNode = (node) => String(node?.querySelector?.('.sales-order-category-key')?.value || '');
22261      const commitDrop = (currentDrag) => {
22262        if (!currentDrag?.targetNode) return;
22263        const currentLots = [...(this.state.cabanaSalesOrderLots || [])];
22264        let ordered = currentLots;
22265        if (currentDrag.kind === 'lot') {
22266          const draggedCert = certOfNode(currentDrag.node);
22267          const targetCert = certOfNode(currentDrag.targetNode);
22268          if (!draggedCert || !targetCert || draggedCert === targetCert) return;
22269          const fromIndex = currentLots.findIndex((lot) => String(this.cabanaLotId(lot)) === draggedCert);
22270          if (fromIndex < 0) return;
22271          const [movedLot] = ordered.splice(fromIndex, 1);
22272          const targetIndex = ordered.findIndex((lot) => String(this.cabanaLotId(lot)) === targetCert);
22273          if (targetIndex < 0) return;
22274          ordered.splice(targetIndex + (currentDrag.dropBefore ? 0 : 1), 0, movedLot);
22275        } else {
22276          const draggedKey = categoryKeyOfNode(currentDrag.node);
22277          const targetKey = categoryKeyOfNode(currentDrag.targetNode);
22278          if (!draggedKey || !targetKey || draggedKey === targetKey) return;
22279          const categoryKeys = [];
22280          const groupedLots = new Map();
22281          currentLots.forEach((lot) => {
22282            const key = this.cabanaSalesCategoryKey(lot);
22283            if (!groupedLots.has(key)) {
22284              categoryKeys.push(key);
22285              groupedLots.set(key, []);
22286            }
22287            groupedLots.get(key).push(lot);
22288          });
22289          const fromIndex = categoryKeys.indexOf(draggedKey);
22290          if (fromIndex < 0) return;
22291          categoryKeys.splice(fromIndex, 1);
22292          const targetIndex = categoryKeys.indexOf(targetKey);
22293          if (targetIndex < 0) return;
22294          categoryKeys.splice(targetIndex + (currentDrag.dropBefore ? 0 : 1), 0, draggedKey);
22295          ordered = categoryKeys.flatMap((key) => groupedLots.get(key) || []);
22296        }
22297        this.setState({
22298          cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(ordered),
22299          cabanaSalesOrderError: '',
22300          cabanaSalesOrderSaved: false,
22301        });
22302      };
22303      let drag = null;
22304      let target = null;
22305      const clearTarget = () => {
22306        target?.classList?.remove('sales-order-native-target');
22307        target = null;
22308      };
22309      const clearDrag = () => {
22310        clearTarget();
22311        if (drag?.node) drag.node.classList.remove('sales-order-native-drag', 'sales-order-drag-source');
22312        drag?.placeholder?.remove?.();
22313        drag?.preview?.remove?.();
22314        document.body.style.removeProperty('user-select');
22315        document.body.classList.remove('sales-order-is-dragging');
22316        root.classList.remove('is-sorting');
22317        drag = null;
22318      };
22319      const positionPreview = (event) => {
22320        if (!drag?.preview) return;
22321        drag.preview.style.transform = `translate3d(${event.clientX - drag.offsetX}px, ${event.clientY - drag.offsetY}px, 0)`;
22322      };
22323      const beginPointerDrag = (event) => {
22324        const rect = drag.node.getBoundingClientRect();
22325        const placeholder = document.createElement('div');
22326        placeholder.className = 'sales-order-drag-placeholder';
22327        placeholder.style.height = `${rect.height}px`;
22328        placeholder.style.width = `${rect.width}px`;
22329        drag.node.parentNode?.insertBefore(placeholder, drag.node);
22330        const preview = drag.node.cloneNode(true);
22331        preview.removeAttribute('data-sales-lot');
22332        preview.removeAttribute('data-sales-category');
22333        preview.classList.add('sales-order-native-preview');
22334        preview.style.width = `${rect.width}px`;
22335        preview.style.height = `${rect.height}px`;
22336        document.body.appendChild(preview);
22337        drag.active = true;
22338        drag.placeholder = placeholder;
22339        drag.preview = preview;
22340        drag.offsetX = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
22341        drag.offsetY = Math.max(0, Math.min(rect.height, event.clientY - rect.top));
22342        drag.node.classList.add('sales-order-native-drag', 'sales-order-drag-source');
22343        document.body.style.userSelect = 'none';
22344        document.body.classList.add('sales-order-is-dragging');
22345        root.classList.add('is-sorting');
22346        try { drag.node.setPointerCapture?.(event.pointerId); } catch (_) {}
22347        positionPreview(event);
22348      };
22349      const resolveDragNode = (eventTarget) => {
22350        const categoryHead = eventTarget.closest?.('.sales-order-category-head');
22351        if (categoryHead) {
22352          const category = categoryHead.closest('[data-sales-category]');
22353          if (category) return { node: category, kind: 'category' };
22354        }
22355        const row = eventTarget.closest?.('[data-sales-lot]');
22356        if (row) return { node: row, kind: 'lot' };
22357        return null;
22358      };
22359      const resolveTarget = (clientX, clientY) => {
22360        if (!drag?.node) return null;
22361        const selector = drag.kind === 'category' ? '[data-sales-category]' : '[data-sales-lot]';
22362        const sourceCategory = drag.kind === 'lot' && this.state.cabanaSalesOrderGrouped
22363          ? drag.node.closest?.('[data-sales-category]')
22364          : null;
22365        const candidates = [...root.querySelectorAll(selector)].filter((candidate) => {
22366          if (candidate === drag.node) return false;
22367          if (sourceCategory && candidate.closest?.('[data-sales-category]') !== sourceCategory) return false;
22368          return true;
22369        });
22370        if (!candidates.length) return null;
22371        const direct = candidates.find((candidate) => {
22372          const rect = candidate.getBoundingClientRect();
22373          return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
22374        });
22375        if (direct) return direct;
22376        return candidates.reduce((nearest, candidate) => {
22377          const rect = candidate.getBoundingClientRect();
22378          const distance = Math.abs(clientY - (rect.top + rect.height / 2));
22379          return !nearest || distance < nearest.distance ? { candidate, distance } : nearest;
22380        }, null)?.candidate || null;
22381      };
22382      const moveAtPointer = (candidate, clientY) => {
22383        if (!candidate || !drag) return;
22384        if (target !== candidate) {
22385          clearTarget();
22386          target = candidate;
22387          target.classList.add('sales-order-native-target');
22388        }
22389        const rect = candidate.getBoundingClientRect();
22390        drag.targetNode = candidate;
22391        drag.dropBefore = clientY < rect.top + (rect.height / 2);
22392        const parent = candidate.parentNode;
22393        if (parent && drag.placeholder) {
22394          parent.insertBefore(drag.placeholder, drag.dropBefore ? candidate : candidate.nextSibling);
22395        }
22396      };
22397      const onPointerMove = (event) => {
22398        if (!drag || event.pointerId !== drag.pointerId) return;
22399        const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
22400        if (!drag.active && distance < 6) return;
22401        if (!drag.active) beginPointerDrag(event);
22402        event.preventDefault();
22403        positionPreview(event);
22404        moveAtPointer(resolveTarget(event.clientX, event.clientY), event.clientY);
22405        if (event.clientY < 86) window.scrollBy(0, -16);
22406        else if (event.clientY > window.innerHeight - 86) window.scrollBy(0, 16);
22407      };
22408      const finishPointerDrag = (event) => {
22409        if (!drag || (event?.pointerId != null && event.pointerId !== drag.pointerId)) return;
22410        const completedDrag = drag.active ? { ...drag } : null;
22411        clearDrag();
22412        window.removeEventListener('pointermove', onPointerMove);
22413        window.removeEventListener('pointerup', finishPointerDrag);
22414        window.removeEventListener('pointercancel', finishPointerDrag);
22415        if (completedDrag) commitDrop(completedDrag);
22416      };
22417      const onPointerDown = (event) => {
22418        if (event.pointerType === 'mouse' && event.button !== 0) return;
22419        if (event.pointerType !== 'mouse' && event.isPrimary === false) return;
22420        if (event.target.closest?.('button,a,select,textarea,[contenteditable="true"]')) return;
22421        const resolved = resolveDragNode(event.target);
22422        if (!resolved) return;
22423        event.preventDefault();
22424        drag = {
22425          ...resolved,
22426          pointerId: event.pointerId,
22427          startX: event.clientX,
22428          startY: event.clientY,
22429          active: false,
22430        };
22431        window.addEventListener('pointermove', onPointerMove, { passive: false });
22432        window.addEventListener('pointerup', finishPointerDrag);
22433        window.addEventListener('pointercancel', finishPointerDrag);
22434      };
22435      root.addEventListener('pointerdown', onPointerDown);
22436      this._cabanaSalesOrderCleanup = () => {
22437        if (this._cabanaSalesOrderBindTimer) clearTimeout(this._cabanaSalesOrderBindTimer);
22438        this._cabanaSalesOrderBindTimer = null;
22439        root.removeEventListener('pointerdown', onPointerDown);
22440        window.removeEventListener('pointermove', onPointerMove);
22441        window.removeEventListener('pointerup', finishPointerDrag);
22442        window.removeEventListener('pointercancel', finishPointerDrag);
22443        clearDrag();
22444      };
22445    });
22446  }
22447  bindCabanaSalesOrderSorting(attempt = 0) {
22448    if (attempt === 0) this.destroyCabanaSalesOrderSorting();
22449    if (this.state.route !== 'cabanaSalesOrder') return;
22450    requestAnimationFrame(() => {
22451      if (this.state.route !== 'cabanaSalesOrder') return;
22452      const root = document.querySelector('.sales-order-page');
22453      const grouped = !!this.state.cabanaSalesOrderGrouped;
22454      const list = root?.querySelector(grouped ? '[data-sales-groups]' : '[data-sales-flat]');
22455      const hasLots = (this.state.cabanaSalesOrderLots || []).length > 0;
22456      if (!root || (hasLots && !list) || typeof window.Sortable !== 'function') {
22457        if (attempt < 30) {
22458          this._cabanaSalesOrderBindTimer = setTimeout(() => {
22459            this._cabanaSalesOrderBindTimer = null;
22460            this.bindCabanaSalesOrderSorting(attempt + 1);
22461          }, 60);
22462        } else {
22463          this.setState({ cabanaSalesOrderError: 'No se pudo activar el ordenamiento. Recargá esta pantalla.' });
22464        }
22465        return;
22466      }
22467
22468      const instances = [];
22469      const certOf = (node) => String(node?.querySelector?.('.sales-order-cert')?.value || '');
22470      const syncFromDom = () => {
22471        const lots = [...(this.state.cabanaSalesOrderLots || [])];
22472        const byCert = new Map(lots.map((lot) => [String(this.cabanaLotId(lot)), lot]));
22473        const certs = grouped
22474          ? [...list.querySelectorAll(':scope > [data-sales-category]')].flatMap((category) =>
22475              [...category.querySelectorAll(':scope > .sales-order-category-list > [data-sales-lot]')].map(certOf)
22476            )
22477          : [...list.querySelectorAll(':scope > [data-sales-lot]')].map(certOf);
22478        const seen = new Set();
22479        const ordered = certs
22480          .map((cert) => {
22481            seen.add(cert);
22482            return byCert.get(cert);
22483          })
22484          .filter(Boolean);
22485        lots.forEach((lot) => {
22486          if (!seen.has(String(this.cabanaLotId(lot)))) ordered.push(lot);
22487        });
22488        this.setState({
22489          cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(ordered),
22490          cabanaSalesOrderError: '',
22491          cabanaSalesOrderSaved: false,
22492        });
22493      };
22494      const common = {
22495        animation: 180,
22496        forceFallback: true,
22497        fallbackOnBody: true,
22498        fallbackTolerance: 4,
22499        swapThreshold: 0.5,
22500        invertSwap: true,
22501        invertedSwapThreshold: 0.35,
22502        ghostClass: 'sales-order-sortable-ghost',
22503        chosenClass: 'sales-order-sortable-chosen',
22504        dragClass: 'sales-order-sortable-drag',
22505        onStart: () => document.body.classList.add('sales-order-is-dragging'),
22506        onEnd: () => {
22507          document.body.classList.remove('sales-order-is-dragging');
22508          syncFromDom();
22509        },
22510      };
22511
22512      if (grouped) {
22513        instances.push(window.Sortable.create(list, {
22514          ...common,
22515          draggable: '[data-sales-category]',
22516          handle: '.sales-order-category-head',
22517        }));
22518        list.querySelectorAll(':scope > [data-sales-category] > .sales-order-category-list').forEach((categoryList) => {
22519          instances.push(window.Sortable.create(categoryList, {
22520            ...common,
22521            draggable: '[data-sales-lot]',
22522          }));
22523        });
22524      } else if (list) {
22525        instances.push(window.Sortable.create(list, {
22526          ...common,
22527          draggable: '[data-sales-lot]',
22528        }));
22529      }
22530
22531      this._cabanaSalesOrderCleanup = () => {
22532        if (this._cabanaSalesOrderBindTimer) clearTimeout(this._cabanaSalesOrderBindTimer);
22533        this._cabanaSalesOrderBindTimer = null;
22534        instances.forEach((instance) => {
22535          try { instance.destroy(); } catch (_) {}
22536        });
22537        document.body.classList.remove('sales-order-is-dragging');
22538      };
22539    });
22540  }
22541  async saveCabanaSalesOrder() {
22542    if (!this.canManageUsers() || this.state.cabanaSalesOrderSaving) return;
22543    const auctionId = this.cabanaRemateId(this.state.currentCabana);
22544    const ordered = this.normalizeCabanaSalesOrder(this.state.cabanaSalesOrderLots || []);
22545    const orders = ordered.map((lot, index) => ({ lotCert: this.cabanaLotId(lot), displayOrder: index + 1 })).filter((item) => item.lotCert);
22546    if (!auctionId || !orders.length) {
22547      this.setState({ cabanaSalesOrderError: 'No hay lotes para ordenar.' });
22548      return;
22549    }
22550    this.setState({ cabanaSalesOrderSaving: true, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false });
22551    try {
22552      await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/reorder', {
22553        method: 'PUT',
22554        body: JSON.stringify({ categoryId: null, orders }),
22555      });
22556      this.setState({ cabanaLots: ordered, cabanaSalesOrderLots: ordered, cabanaSalesOrderOpen: false, cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: true });
22557    } catch (error) {
22558      this.setState({ cabanaSalesOrderSaving: false, cabanaSalesOrderError: error?.message || 'No se pudo guardar el orden de ventas.' });
22559    }
22560  }
22561  openCabana(item, options = {}) {
22562    const requestedPath = window.location.pathname;
22563    item = this.normalizeCabanaAuctionRecord(item);
22564    if (this.cabanaCatalogLocked(item)) {
22565      if (options.syncPath !== false) this.syncCabanaLocation(item);
22566      this.setState({ currentCabana: item, cabanaLots: null, cabanaLotsLoading: false });
22567      this.loginForCabanaCatalog();
22568      return Promise.resolve([]);
22569    }
22570    if (options.syncPath !== false) this.syncCabanaLocation(item);
22571    this.setState({ route: 'cabanaDetail', routeLoading: false, currentCabana: item, cabanaLots: null, cabanaLotsLoading: true, cbQuery: '', cbCat: 'todos', currentCabanaLot: null, cabanaAnalyticsSummary: null, cabanaStatsOverview: false, cabanaSalesOrderOpen: false, cabanaSalesOrderLots: [], cabanaSalesOrderError: '', cabanaPrebidPanelAllowed: fal
22571se, cabanaPrebidPanelOpen: false, cabanaPrebidPanelLoading: false, cabanaPrebidPanelError: '', cabanaPrebidPanelData: null, cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all', cabanaPrebidPanelLot: '', cabanaPublicDeepLoading: !!options.focusLotCert || (this.state.publicAdvanceMode === 'lacassina' && !!options.focusRp), cabanaBrandPreparing: false });
22572    window.scrollTo(0, 0);
22573    const openSalesOrderWhenReady = (lots) => {
22574      if (options.syncPath === false && window.location.pathname !== requestedPath) return lots;
22575      if (this.cabanaRemateId(this.state.currentCabana) !== this.cabanaRemateId(item) || this.state.route !== 'cabanaDetail') return lots;
22576      const itemId = this.cabanaRemateId(item);
22577      if (options.openSalesOrder && this.state.route === 'cabanaDetail' && this.cabanaRemateId(this.state.currentCabana) === itemId) {
22578        this.openCabanaSalesOrder(lots);
22579      }
22580      if (options.focusRp && this.state.publicAdvanceMode === 'lacassina') {
22581        this.openCabanaLotByRp(options.focusRp, lots, options.focusLotNumber);
22582      }
22583      if (options.focusLotCert && !this.cabanaCatalogLocked(item)) {
22584        if (!this.openCabanaLotByCert(options.focusLotCert, lots)) {
22585          this.setState({ cabanaPublicDeepLoading: false });
22586          this.showToast('No se encontró el lote solicitado.');
22587        }
22588      }
22589      return lots;
22590    };
22591    const loading = this.fetchCabanaLots(item).then(openSalesOrderWhenReady);
22592    this.checkCabanaPrebidSummary(item);
22593    this.loadCabanaCommercialTerms(item);
22594    if (this.state.user) this.fetchCabanaPrebidPanel(item, { probe: true });
22595    this.fetchCabanaAnalyticsSummary(item);
22596    this.ensureCabanaBrand(item);
22597    const id = this.cabanaRemateId(item);
22598    if (id && (!item.__detailLoadedAt || Date.now() - item.__detailLoadedAt > 30000)) this.fetchCabanaById(id, { force: true }).then((fresh) => {
22599      if (fresh && this.state.route === 'cabanaDetail' && this.cabanaRemateId(this.state.currentCabana) === id) {
22600        this.setState({ currentCabana: fresh });
22601        this.fetchCabanaLots(fresh);
22602        this.fetchCabanaAnalyticsSummary(fresh);
22603        this.ensureCabanaBrand(fresh);
22604        if (this.state.user) this.fetchCabanaPrebidPanel(fresh, { probe: true });
22605      }
22606    }).catch(() => {});
22607    return loading;
22608  }
22609  openCabanaLotByRp(rp, lots = this.state.cabanaLots, lotNumber = '') {
22610    const targetRp = this.consKey(rp);
22611    const targetLot = this.consKey(lotNumber);
22612    if (!targetRp || !Array.isArray(lots)) return false;
22613    const match = lots.find((lot) =>
22614      this.consKey(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || '') === targetRp &&
22615      (!targetLot || this.consKey(this.cabanaLotNumber(lot)) === targetLot)
22616    ) || lots.find((lot) => this.consKey(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || '') === targetRp);
22617    if (!match) {
22618      if (this.state.publicAdvanceMode === 'lacassina') this.setState({ cabanaPublicDeepLoading: false });
22619      return false;
22620    }
22621    const matchKey = this.cabanaLotGroupKey(match);
22622    const variants = lots.filter((lot) => this.cabanaLotGroupKey(lot) === matchKey);
22623    this.openCabanaLot(match, variants.length ? variants : [match]);
22624    return true;
22625  }
22626  openCabanaLotByCert(lotCert, lots = this.state.cabanaLots) {
22627    const target = String(lotCert || '').trim();
22628    if (!target || !Array.isArray(lots)) return false;
22629    const match = lots.find((lot) => this.cabanaPrebidCert(lot) === target || this.cabanaLotId(lot) === target);
22630    if (!match) return false;
22631    const matchKey = this.cabanaLotGroupKey(match);
22632    const variants = lots.filter((lot) => this.cabanaLotGroupKey(lot) === matchKey);
22633    this.openCabanaLot(match, variants.length ?variants : [match]);
22634    return true;
22635  }
22636  cabanaAnalyticsMetric(summary, keys) {
22637    const root = summary?.data || summary || {};
22638    const sources = [root?.totals, root?.auction, root, root?.metrics, root?.counters].filter(Boolean);
22639    for (const source of sources) {
22640      for (const key of keys) {
22641        const value = Number(source?.[key]);
22642        if (Number.isFinite(value)) return value;
22643      }
22644    }
22645    return 0;
22646  }
22647  cabanaAnalyticsTotals(summary) {
22648    const whatsapp = this.cabanaAnalyticsMetric(summary, ['whatsappClicks', 'whatsAppClicks']);
22649    const calls = this.cabanaAnalyticsMetric(summary, ['callClicks', 'calls']);
22650    const directContacts = this.cabanaAnalyticsMetric(summary, ['contactClicks', 'contacts']);
22651    const durationSamples = this.cabanaAnalyticsMetric(summary, ['viewDurationSamples', 'durationSamples']);
22652    const durationTotal = this.cabanaAnalyticsMetric(summary, ['viewDurationMsTotal', 'durationMsTotal', 'totalDurationMs']);
22653    const avgDuration = this.cabanaAnalyticsMetric(summary, ['avgVie
22653wDurationMs', 'averageViewDurationMs']) || (durationSamples ?durationTotal / durationSamples : 0);
22654    return {
22655      events: this.cabanaAnalyticsMetric(summary, ['eventCount', 'events', 'totalEvents']),
22656      lotViews: this.cabanaAnalyticsMetric(summary, ['lotViews', 'views', 'viewCount']),
22657      rpViews: this.cabanaAnalyticsMetric(summary, ['rpViews']),
22658      videoPlays: this.cabanaAnalyticsMetric(summary, ['videoPlays', 'plays']),
22659      imageViews: this.cabanaAnalyticsMetric(summary, ['imageViews']),
22660      shares: this.cabanaAnalyticsMetric(summary, ['shares', 'shareClicks', 'event_share_click']),
22661      contacts: directContacts || (whatsapp + calls),
22662      avgDuration,
22663    };
22664  }
22665  async openCabanaStatsIndex() {
22666    try { window.history.replaceState({}, '', '/cabanas/estadisticas'); } catch {}
22667    this.setState({
22668      route: 'cabanaStats',
22669      currentCabana: null,
22670      cabanaLots: null,
22671      cabanaStatsOverview: true,
22672      cabanaStatsOverviewLoading: true,
22673      cabanaStatsOverviewError: '',
22674      cabanaStatsOverviewRows: [],
22675    });
22676    window.scrollTo(0, 0);
22677    try {
22678      const remates = Array.isArray(this.state.cabanas) && this.state.cabanas.length ?this.state.cabanas : (await this.fetchCabanas(true) || []);
22679      const allowedRemates = remates.filter((item) => this.canSeeCabanaStats(item));
22680      const rows = await Promise.all(allowedRemates.map(async (item) => {
22681        const auctionId = this.cabanaRemateId(item);
22682        try {
22683          const summary = await this.fetchCabanaAnalyticsSummaryById(auctionId);
22684          return { item, auctionId, ok: true, totals: this.cabanaAnalyticsTotals(summary) };
22685        } catch (error) {
22686          return { item, auctionId, ok: false, totals: this.cabanaAnalyticsTotals(null), error: error?.message || 'Analytics no disponible' };
22687        }
22688      }));
22689      if (this.state.route === 'cabanaStats' && this.state.cabanaStatsOverview) {
22690        this.setState({ cabanaStatsOverviewRows: rows, cabanaStatsOverviewLoading: false, cabanaStatsOverviewError: '' });
22691      }
22692    } catch (error) {
22693      if (this.state.route === 'cabanaStats' && this.state.cabanaStatsOverview) {
22694        this.setState({ cabanaStatsOverviewRows: [], cabanaStatsOverviewLoading: false, cabanaStatsOverviewError: error?.message || 'No se pudieron cargar las estadísticas.' });
22695      }
22696    }
22697  }
22698  openCabanaStats(item) {
22699    item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana);
22700    if (!item) return;
22701    if (!this.canSeeCabanaStats(item)) {
22702      this.showToast('No tenés permiso para ver las estadísticas de este remate.');
22703      return;
22704    }
22705    if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/estadisticas') {
22706      window.location.assign('/lacassina/estadisticas');
22707      return;
22708    }
22709    if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'estadisticas');
22710    const itemId = this.cabanaRemateId(item);
22711    const hasLots = this.cabanaRemateId(this.state.currentCabana) === itemId && Array.isArray(this.state.cabanaLots);
22712    this.setState({
22713      route: 'cabanaStats',
22714      currentCabana: item,
22715      cabanaLots: hasLots ?this.state.cabanaLots : null,
22716      cabanaLotsLoading: !hasLots,
22717      cabanaAnalyticsSummary: null,
22718      cabanaAnalyticsLoading: true,
22719      cabanaStatsVisitorPage: 1,
22720      cabanaStatsOverview: false,
22721      cabanaSalesOrderOpen: false,
22722      profileEditorOpen: false,
22723      profilePhotoViewerOpen: false,
22724      profileCropOpen: false,
22725      profileCropSource: '',
22726      adminUsersOpen: false,
22727      notificationsOpen: false,
22728    });
22729    if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/estadisticas') {
22730      this.writeAppHistory('/lacassina/estadisticas');
22731      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/estadisticas';
22732      window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina-stats';
22733      document.documentElement.dataset.publicRoute = 'lacassina-stats';
22734    }
22735    window.scrollTo(0, 0);
22736    if (!hasLots) this.fetchCabanaLots(item);
22737    this.fetchCabanaAnalyticsSummary(item);
22738    this.ensureCabanaBrand(item);
22739    const id = itemId;
22740    if (id) this.fetchCabanaById(id, { force: true }).then((fresh) => {
22741      if (fresh && this.state.route === 'cabanaStats') {
22742        this.setState({ currentCabana: fresh });
22743        this.fetchCabanaLots(fresh);
22744        this.fetchCabanaAnalyticsSummary(fresh);
22745        this.ensureCabanaBrand(fresh);
22746      }
22747    }).catch(() => {});
22748  }
22749  backToCabanaFromStats() {
22750    if (this.state.cabanaStatsOverview) {
22751      try { window.history.pushState({}, '', '/cabanas'); } catch {}
22752      this.setState({ route: 'cabanas', cabanaStatsOverview: false, currentCabana: null, cabanaLots: null });
22753      if (!Array.isArray(this.state.cabanas)) this.fetchCabanas();
22754      window.scrollTo(0, 0);
22755      return;
22756    }
22757    if (this.state.publicAdvanceMode === 'lacassina') {
22758      this.writeAppHistory('/lacassina');
22759      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina';
22760      window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina';
22761      document.documentElement.dataset.publicRoute = 'lacassina';
22762    } else this.syncCabanaLocation(this.state.currentCabana);
22763    this.setState({ route: 'cabanaDetail' });
22764    window.scrollTo(0, 0);
22765  }
22766  backToCabanaCatalog() {
22767    if (this.state.publicAdvanceMode === 'lacassina') {
22768      this.writeAppHistory('/lacassina');
22769      window.__ROSGAN_PUBLIC_PATH__ = '/lacassina';
22770      window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina';
22771      document.documentElement.dataset.publicRoute = 'lacassina';
22772    } else this.syncCabanaLocation(this.state.currentCabana);
22773    this.setState({ route: 'cabanaDetail' });
22774    window.scrollTo(0, 0);
22775  }
22776  cabanaStatsData(remate = this.state.currentCabana, lots = this.state.cabanaLots) {
22777    if (this.state.cabanaStatsOverview) {
22778      const rows = (Array.isArray(this.state.cabanaStatsOverviewRows) ?this.state.cabanaStatsOverviewRows : []).map((row) => ({
22779        id: row.auctionId,
22780        title: row.item?.nombre_remate || row.item?.titulo || row.item?.title || ('Remate ' + row.auctionId),
22781        events: Number(row.totals?.events || 0).toLocaleString('es-AR'),
22782        views: Number((row.totals?.lotViews || 0) + (row.totals?.rpViews || 0)).toLocaleString('es-AR'),
22783        videos: Number(row.totals?.videoPlays || 0).toLocaleString('es-AR'),
22784        contacts: Number(row.totals?.contacts || 0).toLocaleString('es-AR'),
22785        status: row.ok ?'Registrando' : 'Sin conexión',
22786        statusClass: row.ok ?'is-ok' : 'is-error',
22787        open: () => this.openCabanaStats(row.item),
22788      }));
22789      const totals = (this.state.cabanaStatsOverviewRows || []).reduce((acc, row) => {
22790        Object.keys(acc).forEach((key) => { acc[key] += Number(row.totals?.[key] || 0); });
22791        return acc;
22792      }, { events: 0, lotViews: 0, rpViews: 0, videoPlays: 0, contacts: 0 });
22793      return {
22794        cbsIsOverview: true,
22795        cbsIsAuction: false,
22796        cbsAccent: '#0a5ca8',
22797        cbsDark: '#002e5f',
22798        cbsOverviewLoading: !!this.state.cabanaStatsOverviewLoading,
22799        cbsOverviewReady: !this.state.cabanaStatsOverviewLoading,
22800        cbsOverviewRows: rows,
22801        cbsOverviewHasRows: rows.length > 0,
22802        cbsOverviewEmpty: !rows.length && !this.state.cabanaStatsOverviewLoading,
22803        cbsOverviewError: this.state.cabanaStatsOverviewError || '',
22804        cbsOverviewHasError: !!this.state.cabanaStatsOverviewError,
22805        cbsOverviewAuctionCount: rows.length.toLocaleString('es-AR'),
22806        cbsOverviewEventCount: totals.events.toLocaleString('es-AR'),
22807        cbsOverviewViewCount: (totals.lotViews + totals.rpViews).toLocaleString('es-AR'),
22808        cbsOverviewVideoCount: totals.videoPlays.toLocaleString('es-AR'),
22809        cbsOverviewContactCount: totals.contacts.toLocaleString('es-AR'),
22810        cbsBack: () => this.backToCabanaFromStats(),
22811      };
22812    }
22813    const loading = !!this.state.cabanaLotsLoading || !Array.isArray(lots);
22814    const rows = Array.isArray(lots) ?lots : [];
22815    const hasValue = (value) => value !== null && value !== undefined && String(value).trim() !== '';
22816    const getValue = (lot, keys) => {
22817      for (const key of keys) {
22818        const value = lot?.values?.[key] ?? lot?.[key];
22819        if (hasValue(value)) return value;
22820      }
22821      return null;
22822    };
22823    const numberOf = (value) => {
22824      if (!hasValue(value)) return null;
22825      const normalized = String(value).trim().replace(/\s/g, '').replace(/\.(?=\d{3}(?:\D|$))/g, '').replace(',', '.').replace(/[^\d.+-]/g, '');
22826      const parsed = Number(normalized);
22827      return Number.isFinite(parsed) ?parsed : null;
22828    };
22829    const average = (values) => {
22830      const valid = values.map(numberOf).filter((value) => value !== null);
22831      return valid.length ?valid.reduce((sum, value) => sum + value, 0) / valid.length : null;
22832    };
22833    const categoryCounts = new Map();
22834    const sireCounts = new Map();
22835    const groupIds = new Set();
22836    let mediaCount = 0;
22837    let populatedFields = 0;
22838    let totalFields = 0;
22839    rows.forEach((lot, index) => {
22840      const category = String(lot?.categoryName || lot?.categoriaNombre || lot?.category || 'Sin categoría').trim();
22841      categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
22842      const sire = String(getValue(lot, ['padre', 'sire', 'father']) || '').trim();
22843      if (sire) sireCounts.set(sire, (sireCounts.get(sire) || 0) + 1);
22844      groupIds.add(String(lot?.groupId || lot?.group?.id || lot?.lotCert || lot?.id || index));
22845      if (lot?.coverImage?.url || lot?.thumbnailUrl || lot?.mediaUrl || (Array.isArray(lot?.images) && lot.images.length) || (Array.isArray(lot?.videos) && lot.videos.length)) mediaCount += 1;
22846      Object.entries(lot?.values || {}).forEach(([key, value]) => {
22847        if (['id', 'displayOrder'].includes(key)) return;
22848        totalFields += 1;
22849        if (hasValue(value)) populatedFields += 1;
22850      });
22851    });
22852    const count = rows.length;
22853    const categories = [...categoryCounts.entries()].sort((a, b) => b[1] - a[1]).map(([label, value]) => {
22854      const percentage = count ?Math.round((value / count) * 100) : 0;
22855      return { label, value, percentage, width: `${Math.max(percentage, 3)}%` };
22856    });
22857    const sires = [...sireCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6).map(([label, value], index) => ({
22858      label, value, rank: index + 1,
22859    }));
22860    const weightAverage = average(rows.map((lot) => getValue(lot, ['peso_actual', 'pesoActual', 'peso_promedio', 'pesoPromedio', 'peso'])));
22861    const ceAverage = average(rows.map((lot) => getValue(lot, ['ce', 'c_escr', 'circunferencia_escrotal'])));
22862    const mediaCoverage = count ?Math.round((mediaCount / count) * 100) : 0;
22863    const completeness = totalFields ?Math.round((populatedFields / totalFields) * 100) : 0;
22864    const primary = remate ? (this.cabanaRemateParticipants(remate)[0] || null) : null;
22865    const logo = primary?.logo || primary?.logoUrl || primary?.logoDataUrl || primary?.image || primary?.url || '';
22866    const accent = remate ?this.cabanaColor(remate) : '#0a5ca8';
22867    const analyticsSummary = this.state.cabanaAnalyticsSummary;
22868    const analyticsTotals = this.cabanaAnalyticsTotals(analyticsSummary);
22869    const analyticsRoot = analyticsSummary?.data || analyticsSummary || {};
22870    const analyticsRps = this.listFrom(analyticsRoot, ['rps']).filter(Boolean);
22871    const analyticsVisitors = this.listFrom(analyticsRoot, ['visitors']).filter(Boolean);
22872    const formatVisitorDuration = (milliseconds) => {
22873      const seconds = Math.max(0, Math.round(Number(milliseconds || 0) / 1000));
22874      const hours = Math.floor(seconds / 3600);
22875      const minutes = Math.floor((seconds % 3600) / 60);
22876      const rest = seconds % 60;
22877      return hours ?`${hours} h ${String(minutes).padStart(2, '0')} min` : (minutes ?`${minutes} min ${String(rest).padStart(2, '0')} s` : `${rest} s`);
22878    };
22879    const visitorRowsAll = analyticsVisitors.map((visitor) => {
22880      const registered = String(visitor?.type || '').toLowerCase() === 'registered';
22881      const lastSeenDate = visitor?.lastSeenAt ?new Date(visitor.lastSeenAt) : null;
22882      const lotViews = Number(visitor?.lotViews || 0);
22883      const rpViews = Number(visitor?.rpViews || 0);
22884      const videoPlays = Number(visitor?.videoPlays || 0);
22885      const contactClicks = Number(visitor?.contactClicks || 0);
22886      const visitedLots = (Array.isArray(visitor?.lots) ?visitor.lots : []).map((lot) => {
22887        const label = String(lot?.label || (lot?.lotId ?'Lote ' + lot.lotId : 'Lote')).trim();
22888        const rp = String(lot?.rp || '').trim();
22889        return `${label}${rp && !label.toLowerCase().includes(rp.toLowerCase()) ?' · RP ' + rp : ''} (${formatVisitorDuration(lot?.durationMs)})`;
22890      });
22891      return {
22892        name: registered ?(String(visitor?.name || '').trim() || 'Usuario registrado') : 'Anónimo',
22893        email: registered ?(String(visitor?.email || '').trim() || 'Sin correo informado') : '—',
22894        phone: registered ?(String(visitor?.phone || '').trim() || 'Sin teléfono informado') : '—',
22895        type: registered ?'Registrado' : 'Anónimo',
22896        activity: Number(visitor?.eventCount || 0).toLocaleString('es-AR') + ' acciones',
22897        detail: [lotViews ?lotViews + ' lotes' : '', rpViews ?rpViews + ' RPs' : '', videoPlays ?videoPlays + ' videos' : '', contactClicks ?contactClicks + ' contactos' : ''].filter(Boolean).join(' · ') || 'Navegación registrada',
22898        lots: visitedLots.join(' · ') || 'Sin lote identificado',
22899        totalTime: formatVisitorDuration(visitor?.durationMs),
22900        lastSeen: lastSeenDate && !Number.isNaN(lastSeenDate.getTime()) ?lastSeenDate.toLocaleString('es-AR', { dateStyle: 'short', timeStyle: 'short' }) : 'Sin fecha',
22901      };
22902    }).sort((a, b) => {
22903      if (a.type !== b.type) return a.type === 'Registrado' ?-1 : 1;
22904      return 0;
22905    });
22906    const visitorPageSize = 25;
22907    const visitorPageCount = Math.max(1, Math.ceil(visitorRowsAll.length / visitorPageSize));
22908    const visitorPage = Math.min(Math.max(1, Number(this.state.cabanaStatsVisitorPage || 1)), visitorPageCount);
22909    const visitorRows = visitorRowsAll.slice((visitorPage - 1) * visitorPageSize, visitorPage * visitorPageSize);
22910    const formatAnalyticsDuration = (milliseconds) => {
22911      const seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000));
22912      const hours = Math.floor(seconds / 3600);
22913      const minutes = Math.floor((seconds % 3600) / 60);
22914      const rest = seconds % 60;
22915      if (hours > 0) return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(rest).padStart(2, '0');
22916      return minutes + ':' + String(rest).padStart(2, '0');
22917    };
22918    const rpActivityRows = analyticsRps.map((row) => {
22919      const events = Number(row?.eventCount || row?.events || 0);
22920      const lotViews = Number(row?.lotViews || 0);
22921      const durationTotal = Number(row?.viewDurationMsTotal || row?.durationMsTotal || row?.totalDurationMs || 0);
22922      const samples = Number(row?.viewDurationSamples || row?.durationSamples || 0);
22923      const durationAverage = Number(row?.avgViewDurationMs || row?.averageViewDurationMs || 0) || (samples ?durationTotal / samples : 0);
22924      const contacts = Number(row?.contactClicks || row?.contacts || 0) || (Number(row?.whatsappClicks || 0) + Number(row?.callClicks || 0));
22925      const rpViews = Number(row?.rpViews || row?.views || 0);
22926      const videos = Number(row?.videoPlays || row?.plays || 0);
22927      const images = Number(row?.imageViews || 0);
22928      const shares = Number(row?.shares || row?.shareClicks || row?.event_share_click || 0);
22929      return {
22930        rp: String(row?.rp || row?.rpId || row?.itemId || 'Sin RP'),
22931        lot: row?.lotCert ?('Lote ' + row.lotCert) : (row?.lotTitle || 'Sin lote'),
22932        category: String(row?.categoryName || row?.lotTitle || 'Sin categoría'),
22933        lotViews: lotViews.toLocaleString('es-AR'),
22934        rpViews: rpViews.toLocaleString('es-AR'),
22935        videos: videos.toLocaleString('es-AR'),
22936        images: images.toLocaleString('es-AR'),
22937        contacts: contacts.toLocaleString('es-AR'),
22938        shares: shares.toLocaleString('es-AR'),
22939        samplesLabel: samples === 1 ?'1 medición de tiempo' : (samples > 1 ?(samples.toLocaleString('es-AR') + ' mediciones de tiempo') :'Sin mediciones de tiempo'),
22940        totalTime: formatAnalyticsDuration(durationTotal),
22941        avgTime: formatAnalyticsDuration(durationAverage),
22942        durationTotal,
22943        durationAverage,
22944        samples,
22945        lotViewsNumber: lotViews,
22946        rpViewsNumber: rpViews,
22947        videosNumber: videos,
22948        imagesNumber: images,
22949        contactsNumber: contacts,
22950        sharesNumber: shares,
22951        eventsNumber: events,
22952      };
22953    }).filter((row) => row.eventsNumber > 0).sort((a, b) => b.durationTotal - a.durationTotal || b.eventsNumber - a.eventsNumber || a.rp.localeCompare(b.rp, 'es'));
22954    const avgDurationLabel = analyticsTotals.avgDuration > 0 ?this.formatCabanaVideoTime(analyticsTotals.avgDuration / 1000) : '0:00';
22955    const percentage = (numerator, denominator) => denominator > 0 ?(numerator / denominator) * 100 : 0;
22956    const formatPercent = (value) => value.toLocaleString('es-AR', { minimumFractionDigits: value > 0 && value < 10 ?1 : 0, maximumFractionDigits: 1 }) + '%';
22957    const formatDecimal = (value) => value.toLocaleString('es-AR', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
22958    const contactRateNumber = percentage(analyticsTotals.contacts, analyticsTotals.lotViews);
22959    const evaluationDepthNumber = analyticsTotals.lotViews > 0 ?analyticsTotals.rpViews / analyticsTotals.lotViews : 0;
22960    const videoRateNumber = percentage(analyticsTotals.videoPlays, analyticsTotals.rpViews);
22961    const attentionSeconds = analyticsTotals.avgDuration / 1000;
22962    const contactHealth = contactRateNumber >= 3
22963      ?{ label: 'Respuesta activa', className: 'is-good' }
22964      : (contactRateNumber >= 1 ?{ label: 'En desarrollo', className: 'is-watch' } : { label: 'Oportunidad de mejora', className: 'is-alert' });
22965    const depthHealth = evaluationDepthNumber >= 1.5
22966      ?{ label: 'Exploración profunda', className: 'is-good' }
22967      : (evaluationDepthNumber >= 1 ?{ label: 'Exploración media', className: 'is-watch' } : { label: 'Exploración superficial', className: 'is-alert' });
22968    const videoHealth = videoRateNumber >= 35
22969      ?{ label: 'Video decisivo', className: 'is-good' }
22970      : (videoRateNumber >= 20 ?{ label: 'Uso intermedio', className: 'is-watch' } : { label: 'Baja activación', className: 'is-alert' });
22971    const attentionHealth = attentionSeconds >= 20
22972      ?{ label: 'Evaluación detenida', className: 'is-good' }
22973      : (attentionSeconds >= 8 ?{ label: 'Atención breve', className: 'is-watch' } : { label: 'Salida rápida', className: 'is-alert' });
22974
22975    const categoryPerformanceMap = new Map();
22976    categoryCounts.forEach((published, label) => categoryPerformanceMap.set(this.consKey(label), {
22977      label, published, views: 0, videos: 0, contacts: 0, shares: 0, durationTotal: 0, samples: 0,
22978    }));
22979    analyticsRps.forEach((row) => {
22980      const label = String(row?.categoryName || row?.lotTitle || 'Sin categoría');
22981      const key = this.consKey(label);
22982      if (!categoryPerformanceMap.has(key)) categoryPerformanceMap.set(key, { label, published: 0, views: 0, videos: 0, contacts: 0, shares: 0, durationTotal: 0, samples: 0 });
22983      const target = categoryPerformanceMap.get(key);
22984      target.views += Number(row?.rpViews || row?.views || 0);
22985      target.videos += Number(row?.videoPlays || row?.plays || 0);
22986      target.contacts += Number(row?.contactClicks || row?.contacts || 0) || (Number(row?.whatsappClicks || 0) + Number(row?.callClicks || 0));
22987      target.shares += Number(row?.shares || row?.shareClicks || row?.event_share_click || 0);
22988      target.durationTotal += Number(row?.viewDurationMsTotal || row?.durationMsTotal || row?.totalDurationMs || 0);
22989      target.samples += Number(row?.viewDurationSamples || row?.durationSamples || 0);
22990    });
22991    const categoryPerformance = [...categoryPerformanceMap.values()].filter((item) => item.views > 0).map((item) => {
22992      const viewsPerRpNumber = item.published > 0 ?item.views / item.published : item.views;
22993      const avgTimeMs = item.samples ?item.durationTotal / item.samples : 0;
22994      return {
22995        ...item,
22996        viewsPerRpNumber,
22997        viewsNumber: item.views,
22998        videosNumber: item.videos,
22999        contactsNumber: item.contacts,
23000        sharesNumber: item.shares,
23001        intentNumber: item.contacts + item.shares,
23002        avgTimeMs,
23003        viewsPerRp: formatDecimal(viewsPerRpNumber),
23004        views: item.views.toLocaleString('es-AR'),
23005        videos: item.videos.toLocaleString('es-AR'),
23006        videoRate: formatPercent(percentage(item.videos, item.views)),
23007        intent: (item.contacts + item.shares).toLocaleString('es-AR'),
23008        avgTime: formatAnalyticsDuration(avgTimeMs),
23009      };
23010    }).sort((a, b) => b.viewsPerRpNumber - a.viewsPerRpNumber || b.contacts - a.contacts || a.label.localeCompare(b.label, 'es'));
23011
23012    const opportunities = [...rpActivityRows].sort((a, b) => b.rpViewsNumber - a.rpViewsNumber || (b.contactsNumber + b.sharesNumber) - (a.contactsNumber + a.sharesNumber) || b.durationAverage - a.durationAverage).slice(0, 12).map((row) => ({
23013      ...row,
23014      videoRate: formatPercent(percentage(row.videosNumber, row.rpViewsNumber)),
23015      intent: (row.contactsNumber + row.sharesNumber).toLocaleString('es-AR'),
23016    }));
23017
23018    const analyticsDaily = this.listFrom(analyticsRoot, ['daily']).filter(Boolean).sort((a, b) => String(a?.dayKey || '').localeCompare(String(b?.dayKey || ''))).slice(-10);
23019    const dailyValues = analyticsDaily.map((row) => Number(row?.lotViews || 0) + Number(row?.rpViews || 0));
23020    const maxDailyValue = Math.max(1, ...dailyValues);
23021    const trend = analyticsDaily.map((row, index) => {
23022      const rawKey = String(row?.dayKey || '');
23023      const date = /^\d{4}-\d{2}-\d{2}$/.test(rawKey) ?new Date(rawKey + 'T12:00:00') : null;
23024      const value = dailyValues[index];
23025      const intent = (Number(row?.contactClicks || row?.contacts || 0) + Number(row?.shares || row?.shareClicks || 0));
23026      return {
23027        dayKey: rawKey,
23028        label: date && !Number.isNaN(date.getTime()) ?date.toLocaleDateString('es-AR', { day: '2-digit', month: 'short' }).replace('.', '') : rawKey,
23029        valueNumber: value,
23030        value: value.toLocaleString('es-AR'),
23031        width: Math.max(4, Math.round((value / maxDailyValue) * 100)) + '%',
23032        intentNumber: intent,
23033        intent: intent ?(intent.toLocaleString('es-AR') + ' int.') : '—',
23034      };
23035    });
23036
23037    const topRp = [...rpActivityRows].sort((a, b) => b.rpViewsNumber - a.rpViewsNumber)[0] || null;
23038    const topFiveViews = [...rpActivityRows].sort((a, b) => b.rpViewsNumber - a.rpViewsNumber).slice(0, 5).reduce((sum, row) => sum + row.rpViewsNumber, 0);
23039    const concentration = percentage(topFiveViews, analyticsTotals.rpViews);
23040    const topCategory = categoryPerformance[0] || null;
23041    const insights = [
23042      {
23043        index: '01',
23044        title: contactRateNumber < 1 ?'La consulta es el principal cuello de botella' : 'El catálogo ya transforma interés en consultas',
23045        detail: `${analyticsTotals.contacts.toLocaleString('es-AR')} contacto${analyticsTotals.contacts === 1 ?'' : 's'} sobre ${analyticsTotals.lotViews.toLocaleString('es-AR')} aperturas. ${contactRateNumber < 1 ?'Conviene reforzar el llamado a WhatsApp y la propuesta comercial en los RPs con más vistas.' : 'Identificá qué categorías generan esas consultas y replicá su presentación.'}`,
23046      },
23047      {
23048        index: '02',
23049        title: topRp ?`${topRp.rp} concentra la mayor atención individual` :'Todavía no hay un RP destacado',
23050        detail: topRp ?`${topRp.rpViews} evaluaciones en ${topRp.lot}, con ${topRp.videos} reproducciones de video. Los cinco RPs más vistos concentran ${formatPercent(concentration)} de las evaluaciones.` :'Hace falta más actividad para priorizar reproductores.',
23051      },
23052      {
23053        index: '03',
23054        title: topCategory ?`${topCategory.label} lidera la demanda relativa` :'Todavía no hay una categoría líder',
23055        detail: topCategory ?`${topCategory.viewsPerRp} vistas por RP publicado, comparando categorías sin favorecer a las que tienen más animales.` :'La comparación se habilitará cuando existan vistas por categoría.',
23056      },
23057    ];
23058    const exportData = {
23059      title: remate?.nombre_remate || remate?.titulo || remate?.title || 'Remate de cabaña',
23060      accent,
23061      dark: remate ?this.cabanaBrandDark(remate) : '#002e5f',
23062      logo,
23063      rpCount: count,
23064      lotCount: groupIds.size,
23065      categoryCount: categoryCounts.size,
23066      mediaCoverage,
23067      completeness,
23068      weightAverage,
23069      ceAverage,
23070      totals: analyticsTotals,
23071      contactRateNumber,
23072      evaluationDepthNumber,
23073      videoRateNumber,
23074      attentionSeconds,
23075      categories: categories.map((item) => ({ label: item.label, published: item.value, percentage: item.percentage })),
23076      categoryPerformance,
23077      opportunities,
23078      rpActivityRows,
23079      trend,
23080      insights,
23081    };
23082    return {
23083      cbsIsOverview: false,
23084      cbsIsAuction: true,
23085      cbsLoading: loading,
23086      cbsReady: !loading,
23087      cbsTitle: remate?.nombre_remate || remate?.titulo || remate?.title || 'Remate de cabaña',
23088      cbsLogo: logo,
23089      cbsHasLogo: !!logo,
23090      cbsAccent: accent,
23091      cbsDark: remate ?this.cabanaBrandDark(remate) : '#002e5f',
23092      cbsRpCount: count,
23093      cbsLotCount: groupIds.size,
23094      cbsCategoryCount: categoryCounts.size,
23095      cbsMediaCoverage: `${mediaCoverage}%`,
23096      cbsCompleteness: `${completeness}%`,
23097      cbsAvgWeight: weightAverage === null ?'Sin datos' : `${Math.round(weightAverage).toLocaleString('es-AR')} kg`,
23098      cbsAvgCe: ceAverage === null ?'Sin datos' : ceAverage.toLocaleString('es-AR', { maximumFractionDigits: 1 }),
23099      cbsCategories: categories,
23100      cbsHasCategories: categories.length > 0,
23101      cbsNoCategories: categories.length === 0,
23102      cbsSires: sires,
23103      cbsHasSires: sires.length > 0,
23104      cbsNoSires: sires.length === 0,
23105      cbsAnalyticsLoading: !!this.state.cabanaAnalyticsLoading,
23106      cbsAnalyticsReady: !this.state.cabanaAnalyticsLoading,
23107      cbsAnalyticsEventCount: analyticsTotals.events.toLocaleString('es-AR'),
23108      cbsAnalyticsLotViews: analyticsTotals.lotViews.toLocaleString('es-AR'),
23109      cbsAnalyticsRpViews: analyticsTotals.rpViews.toLocaleString('es-AR'),
23110      cbsAnalyticsVideoPlays: analyticsTotals.videoPlays.toLocaleString('es-AR'),
23111      cbsAnalyticsImageViews: analyticsTotals.imageViews.toLocaleString('es-AR'),
23112      cbsAnalyticsShares: analyticsTotals.shares.toLocaleString('es-AR'),
23113      cbsAnalyticsContacts: analyticsTotals.contacts.toLocaleString('es-AR'),
23114      cbsAnalyticsContactLabel: analyticsTotals.contacts === 1 ?'1 contacto' : (analyticsTotals.contacts.toLocaleString('es-AR') + ' contactos'),
23115      cbsAnalyticsAvgDuration: avgDurationLabel,
23116      cbsContactRate: formatPercent(contactRateNumber),
23117      cbsEvaluationDepth: formatDecimal(evaluationDepthNumber),
23118      cbsVideoRate: formatPercent(videoRateNumber),
23119      cbsContactHealth: contactHealth.label,
23120      cbsContactHealthClass: contactHealth.className,
23121      cbsDepthHealth: depthHealth.label,
23122      cbsDepthHealthClass: depthHealth.className,
23123      cbsVideoHealth: videoHealth.label,
23124      cbsVideoHealthClass: videoHealth.className,
23125      cbsAttentionHealth: attentionHealth.label,
23126      cbsAttentionHealthClass: attentionHealth.className,
23127      cbsInsights: insights,
23128      cbsTrend: trend,
23129      cbsHasTrend: trend.length > 0,
23130      cbsNoTrend: trend.length === 0,
23131      cbsCategoryPerformance: categoryPerformance,
23132      cbsHasCategoryPerformance: categoryPerformance.length > 0,
23133      cbsNoCategoryPerformance: categoryPerformance.length === 0,
23134      cbsOpportunities: opportunities,
23135      cbsOpportunityCount: rpActivityRows.length.toLocaleString('es-AR'),
23136      cbsHasOpportunities: opportunities.length > 0,
23137      cbsNoOpportunities: opportunities.length === 0,
23138      cbsRpActivityRows: rpActivityRows,
23139      cbsRpActivityCount: rpActivityRows.length.toLocaleString('es-AR'),
23140      cbsHasRpActivity: rpActivityRows.length > 0,
23141      cbsNoRpActivity: rpActivityRows.length === 0,
23142      cbsVisitors: visitorRows,
23143      cbsVisitorCount: visitorRowsAll.length.toLocaleString('es-AR'),
23144      cbsHasVisitors: visitorRowsAll.length > 0,
23145      cbsNoVisitors: visitorRowsAll.length === 0,
23146      cbsVisitorPage: visitorPage.toLocaleString('es-AR'),
23147      cbsVisitorPageCount: visitorPageCount.toLocaleString('es-AR'),
23148      cbsVisitorPreviousDisabled: visitorPage <= 1,
23149      cbsVisitorNextDisabled: visitorPage >= visitorPageCount,
23150      cbsVisitorPrevious: () => this.setState({ cabanaStatsVisitorPage: Math.max(1, visitorPage - 1) }),
23151      cbsVisitorNext: () => this.setState({ cabanaStatsVisitorPage: Math.min(visitorPageCount, visitorPage + 1) }),
23152      cbsExportData: exportData,
23153      cbsExportDisabled: loading || !!this.state.cabanaAnalyticsLoading || !!this.state.cabanaStatsExportBusy,
23154      cbsExcelLabel: this.state.cabanaStatsExportBusy === 'excel' ?'Generando Excel…' :'Exportar Excel',
23155      cbsStoryLabel: this.state.cabanaStatsExportBusy === 'story' ?'Generando Story…' :'Story 9:16',
23156      cbsDownloadExcel: () => this.downloadCabanaStatsExcel(),
23157      cbsDownloadStory: () => this.downloadCabanaStatsStory(),
23158      cbsBack: () => this.backToCabanaFromStats(),
23159    };
23160  }
23161  async ensureCabanaExcelJs() {
23162    if (window.ExcelJS?.Workbook) return window.ExcelJS;
23163    if (this._cabanaExcelJsPromise) return this._cabanaExcelJsPromise;
23164    const sources = [
23165      '/vendor/exceljs.min.js?v=4.4.0',
23166    ];
23167    this._cabanaExcelJsPromise = (async () => {
23168      for (const src of sources) {
23169        try {
23170          await new Promise((resolve, reject) => {
23171            const script = document.createElement('script');
23172            script.src = src;
23173            script.async = true;
23174            script.onload = resolve;
23175            script.onerror = () => reject(new Error('No se pudo cargar ExcelJS.'));
23176            document.head.appendChild(script);
23177          });
23178          if (window.ExcelJS?.Workbook) return window.ExcelJS;
23179        } catch {}
23180      }
23181      throw new Error('No se pudo preparar el generador de Excel. Revisá la conexión e intentá nuevamente.');
23182    })();
23183    try { return await this._cabanaExcelJsPromise; }
23184    catch (error) { this._cabanaExcelJsPromise = null; throw error; }
23185  }
23186  async cabanaStatsLogoDataUrl(source) {
23187    const url = this.cabanaMediaUrl(source) || String(source || '').trim();
23188    if (!url) return '';
23189    try {
23190      const image = await this.loadCabanaCanvasImage(url);
23191      if (!image) return '';
23192      const canvas = document.createElement('canvas');
23193      canvas.width = 720;
23194      canvas.height = 240;
23195      const ctx = canvas.getContext('2d');
23196      const scale = Math.min(canvas.width / image.naturalWidth, canvas.height / image.naturalHeight);
23197      const width = image.naturalWidth * scale;
23198      const height = image.naturalHeight * scale;
23199      ctx.drawImage(image, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
23200      return canvas.toDataURL('image/png');
23201    } catch { return ''; }
23202  }
23203  cabanaStatsExportSnapshot() {
23204    return this.cabanaStatsData(this.state.currentCabana, this.state.cabanaLots)?.cbsExportData || null;
23205  }
23206  async downloadCabanaStatsExcel() {
23207    if (this.state.cabanaStatsExportBusy) return;
23208    const snapshot = this.cabanaStatsExportSnapshot();
23209    if (!snapshot) { this.showToast('Todavía no hay estadísticas para exportar.'); return; }
23210    this.setState({ cabanaStatsExportBusy: 'excel' });
23211    try {
23212      const ExcelJS = await this.ensureCabanaExcelJs();
23213      const current = this.state.currentCabana || {};
23214      const participants = this.cabanaRemateParticipants(current);
23215      const mainParticipant = participants.find((item) => item.type === 'principal') || participants[0] || {};
23216      const mainLogoSource = mainParticipant.logoDataUrl || mainParticipant.logoUrl || mainParticipant.logo || mainParticipant.imageUrl || '';
23217      const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || '';
23218      const consignatariaName = current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Rosgan';
23219      const consignatariaLogoSource = this.consLogo(consignatariaRaw || consignatariaName) || '';
23220      const [rosganLogo, mainLogo, consignatariaLogo] = await Promise.all([
23221        this.cabanaStatsLogoDataUrl('/assets/rosgan-logo-blue.png'),
23222        this.cabanaStatsLogoDataUrl(mainLogoSource),
23223        this.cabanaStatsLogoDataUrl(consignatariaLogoSource),
23224      ]);
23225      const workbook = new ExcelJS.Workbook();
23226      workbook.creator = 'Rosgan';
23227      workbook.lastModifiedBy = 'Rosgan';
23228      workbook.created = new Date();
23229      workbook.modified = new Date();
23230      workbook.calcProperties.fullCalcOnLoad = true;
23231      const colorArgb = (value, fallback) => {
23232        const raw = String(value || fallback || '').replace('#', '').trim();
23233        const expanded = raw.length === 3 ?raw.split('').map((char) => char + char).join('') : raw;
23234        return /^[0-9a-f]{6}$/i.test(expanded) ?('FF' + expanded.toUpperCase()) : String(fallback || 'FF002E5F').replace('#', '').toUpperCase().padStart(8, 'F');
23235      };
23236      const dark = colorArgb(snapshot.dark, 'FF002E5F');
23237      const accent = colorArgb(snapshot.accent, 'FF0A5CA8');
23238      const pale = 'FFEAF3FA';
23239      const border = 'FFD9E5EF';
23240      const white = 'FFFFFFFF';
23241      const text = 'FF17304D';
23242      const muted = 'FF6D8094';
23243      const setFill = (cell, argb) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb } }; };
23244      const paint = (sheet, r1, c1, r2, c2, argb) => {
23245        for (let row = r1; row <= r2; row += 1) for (let col = c1; col <= c2; col += 1) setFill(sheet.getCell(row, col), argb);
23246      };
23247      const borderStyle = { style: 'thin', color: { argb: border } };
23248      const addLogo = (sheet, dataUrl, position) => {
23249        if (!dataUrl) return false;
23250        const id = workbook.addImage({ base64: dataUrl, extension: 'png' });
23251        sheet.addImage(id, position);
23252        return true;
23253      };
23254      const summary = workbook.addWorksheet('Resumen', { properties: { defaultRowHeight: 20 }, views: [{ showGridLines: false, state: 'frozen', ySplit: 5 }] });
23255      summary.columns = Array.from({ length: 12 }, () => ({ width: 12 }));
23256      paint(summary, 1, 1, 4, 12, dark);
23257      paint(summary, 1, 1, 4, 3, white);
23258      paint(summary, 1, 10, 4, 12, white);
23259      summary.mergeCells('D1:I2');
23260      summary.getCell('D1').value = 'INFORME DE ESTADÍSTICAS';
23261      summary.getCell('D1').font = { name: 'Aptos Display', size: 22, bold: true, color: { argb: white } };
23262      summary.getCell('D1').alignment = { vertical: 'middle', horizontal: 'left' };
23263      summary.mergeCells('D3:I3');
23264      summary.getCell('D3').value = snapshot.title;
23265      summary.getCell('D3').font = { name: 'Aptos', size: 12, bold: true, color: { argb: 'FFDCEAF6' } };
23266      summary.getCell('D3').alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
23267      summary.mergeCells('D4:I4');
23268      summary.getCell('D4').value = `Generado ${new Date().toLocaleString('es-AR')} · rosgan.app`;
23269      summary.getCell('D4').font = { name: 'Aptos', size: 9, color: { argb: 'FFB8CADE' } };
23270      summary.getCell('D4').alignment = { vertical: 'middle', horizontal: 'left' };
23271      summary.getRow(1).height = 28; summary.getRow(2).height = 28; summary.getRow(3).height = 24; summary.getRow(4).height = 22;
23272      if (!addLogo(summary, rosganLogo, { tl: { col: .2, row: .35 }, ext: { width: 190, height: 54 } })) {
23273        summary.mergeCells('A2:C3'); summary.getCell('A2').value = 'ROSGAN'; summary.getCell('A2').font = { size: 20, bold: true, color: { argb: dark } }; summary.getCell('A2').alignment = { horizontal: 'center', vertical: 'middle' };
23274      }
23275      if (!addLogo(summary, mainLogo, { tl: { col: 9.1, row: .2 }, ext: { width: 190, height: 58 } })) {
23276        summary.mergeCells('J1:L2'); summary.getCell('J1').value = mainParticipant.name || snapshot.title; summary.getCell('J1').font = { size: 11, bold: true, color: { argb: dark } }; summary.getCell('J1').alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
23277      }
23278      if (!addLogo(summary, consignatariaLogo, { tl: { col: 9.25, row: 2.25 }, ext: { width: 170, height: 42 } })) {
23279        summary.mergeCells('J3:L4'); summary.getCell('J3').value = consignatariaName; summary.getCell('J3').font = { size: 10, bold: true, color: { argb: accent } }; summary.getCell('J3').alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
23280      }
23281      summary.mergeCells('A6:L6');
23282      summary.getCell('A6').value = 'RESUMEN EJECUTIVO';
23283      summary.getCell('A6').font = { size: 11, bold: true, color: { argb: accent } };
23284      summary.getCell('A6').alignment = { vertical: 'middle' };
23285      const kpis = [
23286        { label: 'APERTURAS DE LOTE', value: snapshot.totals.lotViews, note: `${snapshot.lotCount} lotes publicados`, format: '#,##0' },
23287        { label: 'EVALUACIONES DE RP', value: snapshot.totals.rpViews, note: `${snapshot.rpCount} RPs en catálogo`, format: '#,##0' },
23288        { label: 'REPRODUCCIONES', value: snapshot.totals.videoPlays, note: 'Videos iniciados', format: '#,##0' },
23289        { label: 'CONTACTOS', value: snapshot.totals.contacts, note: 'WhatsApp y llamadas', format: '#,##0' },
23290      ];
23291      kpis.forEach((item, index) => {
23292        const start = 1 + index * 3;
23293        const end = start + 2;
23294        summary.mergeCells(7, start, 7, end); summary.mergeCells(8, start, 8, end); summary.mergeCells(9, start, 9, end);
23295        paint(summary, 7, start, 9, end, pale);
23296        for (let col = start; col <= end; col += 1) {
23297          summary.getCell(7, col).border = { top: borderStyle, left: col === start ?borderStyle : undefined, right: col === end ?borderStyle : undefined };
23298          summary.getCell(9, col).border = { bottom: borderStyle, left: col === start ?borderStyle : undefined, right: col === end ?borderStyle : undefined };
23299        }
23300        const label = summary.getCell(7, start); label.value = item.label; label.font = { size: 9, bold: true, color: { argb: muted } }; label.alignment = { vertical: 'bottom', horizontal: 'center' };
23301        const value = summary.getCell(8, start); value.value = item.value; value.numFmt = item.format; value.font = { size: 22, bold: true, color: { argb: dark } }; value.alignment = { vertical: 'middle', horizontal: 'center' };
23302        const note = summary.getCell(9, start); note.value = item.note; note.font = { size: 9, color: { argb: muted } }; note.alignment = { vertical: 'top', horizontal: 'center' };
23303      });
23304      summary.getRow(7).height = 22; summary.getRow(8).height = 34; summary.getRow(9).height = 23;
23305      const executiveRows = [
23306        ['Señales registradas', snapshot.totals.events, 'Total de eventos de analítica', '#,##0'],
23307        ['Imágenes vistas', snapshot.totals.imageViews, 'Aperturas de material fotográfico', '#,##0'],
23308        ['Compartidos', snapshot.totals.shares, 'Acciones de difusión registradas', '#,##0'],
23309        ['Conversión a consulta', snapshot.contactRateNumber / 100, 'Contactos sobre aperturas de lote', '0.0%'],
23310        ['Profundidad de evaluación', snapshot.evaluationDepthNumber, 'RPs examinados por apertura', '0.0'],
23311        ['Activación de video', snapshot.videoRateNumber / 100, 'Reproducciones sobre vistas de RP', '0.0%'],
23312        ['Tiempo promedio', snapshot.attentionSeconds, 'Segundos efectivos por evaluación', '0.0 "s"'],
23313        ['Cobertura multimedia', snapshot.mediaCoverage / 100, 'RPs con foto o video', '0%'],
23314        ['Completitud de ficha', snapshot.completeness / 100, 'Campos 
23314informados del catálogo', '0%'],
23315      ];
23316      summary.mergeCells('A11:L11'); summary.getCell('A11').value = 'INDICADORES COMERCIALES'; setFill(summary.getCell('A11'), accent); summary.getCell('A11').font = { size: 10, bold: true, color: { argb: white } }; summary.getCell('A11').alignment = { vertical: 'middle' };
23317      summary.getRow(11).height = 25;
23318      executiveRows.forEach((item, index) => {
23319        const row = 12 + index;
23320        summary.mergeCells(row, 1, row, 4); summary.mergeCells(row, 5, row, 7); summary.mergeCells(row, 8, row, 12);
23321        summary.getCell(row, 1).value = item[0]; summary.getCell(row, 1).font = { bold: true, color: { argb: text } };
23322        summary.getCell(row, 5).value = item[1]; summary.getCell(row, 5).numFmt = item[3]; summary.getCell(row, 5).font = { bold: true, color: { argb: accent } }; summary.getCell(row, 5).alignment = { horizontal: 'right' };
23323        summary.getCell(row, 8).value = item[2]; summary.getCell(row, 8).font = { color: { argb: muted }, italic: true };
23324        paint(summary, row, 1, row, 12, index % 2 ?'FFF7FAFC' : white);
23325        for (let col = 1; col <= 12; col += 1) summary.getCell(row, col).border = { bottom: borderStyle };
23326      });
23327      const insightHeaderRow = 13 + executiveRows.length;
23328      summary.mergeCells(insightHeaderRow, 1, insightHeaderRow, 12); summary.getCell(insightHeaderRow, 1).value = 'LECTURA EJECUTIVA'; setFill(summary.getCell(insightHeaderRow, 1), dark); summary.getCell(insightHeaderRow, 1).font = { size: 10, bold: true, color: { argb: white } };
23329      snapshot.insights.forEach((item, index) => {
23330        const row = insightHeaderRow + 1 + index * 2;
23331        summary.mergeCells(row, 1, row, 12); summary.getCell(row, 1).value = `${item.index} · ${item.title}`; summary.getCell(row, 1).font = { size: 11, bold: true, color: { argb: dark } };
23332        summary.mergeCells(row + 1, 1, row + 1, 12); summary.getCell(row + 1, 1).value = item.detail; summary.getCell(row + 1, 1).font = { size: 9, color: { argb: muted } }; summary.getCell(row + 1, 1).alignment = { wrapText: true, vertical: 'top' }; summary.getRow(row + 1).height = 30;
23333      });
23334      summary.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 1, paperSize: 9, margins: { left: .3, right: .3, top: .4, bottom: .4, header: .2, footer: .2 } };
23335
23336      const prepareDataSheet = (name, title, subtitle, headers, widths) => {
23337        const sheet = workbook.addWorksheet(name, { views: [{ showGridLines: false, state: 'frozen', ySplit: 4 }] });
23338        sheet.columns = widths.map((width) => ({ width }));
23339        sheet.mergeCells(1, 1, 1, headers.length); sheet.getCell(1, 1).value = title; setFill(sheet.getCell(1, 1), dark); sheet.getCell(1, 1).font = { size: 18, bold: true, color: { argb: white } }; sheet.getCell(1, 1).alignment = { vertical: 'middle' }; sheet.getRow(1).height = 34;
23340        sheet.mergeCells(2, 1, 2, headers.length); sheet.getCell(2, 1).value = subtitle; sheet.getCell(2, 1).font = { size: 10, color: { argb: muted } }; sheet.getCell(2, 1).alignment = { vertical: 'middle' };
23341        headers.forEach((header, index) => { const cell = sheet.getCell(4, index + 1); cell.value = header; setFill(cell, accent); cell.font = { size: 9, bold: true, color: { argb: white } }; cell.alignment = { horizontal: index === 0 ?'left' :'center', vertical: 'middle', wrapText: true }; });
23342        sheet.getRow(4).height = 30;
23343        return sheet;
23344      };
23345      const finalizeDataSheet = (sheet, rowCount, columnCount, numberFormats = {}) => {
23346        const endRow = Math.max(4, 4 + rowCount);
23347        sheet.autoFilter = { from: { row: 4, column: 1 }, to: { row: endRow, column: columnCount } };
23348        for (let row = 5; row <= endRow; row += 1) {
23349          if (row % 2 === 0) paint(sheet, row, 1, row, columnCount, 'FFF5F9FC');
23350          for (let col = 1; col <= columnCount; col += 1) {
23351            const cell = sheet.getCell(row, col); cell.border = { bottom: borderStyle }; cell.font = { name: 'Aptos', size: 10, color: { argb: text }, bold: row === endRow && sheet.getCell(row, 1).value === 'TOTAL' }; cell.alignment = { vertical: 'middle', horizontal: col === 1 ?'left' :'right', wrapText: col <= 3 };
23352            if (numberFormats[col]) cell.numFmt = numberFormats[col];
23353          }
23354        }
23355        sheet.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 0, margins: { left: .25, right: .25, top: .45, bottom: .45, header: .2, footer: .2 } };
23356      };
23357      const categoryRows = snapshot.categoryPerformance.length ?snapshot.categoryPerformance : snapshot.categories.map((item) => ({ ...item, viewsNumber: 0, videosNumber: 0, contactsNumber: 0, sharesNumber: 0, intentNumber: 0, avgTimeMs: 0, viewsPerRpNumber: 0 }));
23358      const categorySheet = prepareDataSheet('Categorías', 'Rendimiento por categoría', snapshot.title, ['Categoría', 'RPs publicados', 'Evaluaciones', 'Vistas / RP', 'Videos', 'Activación video', 'Contactos', 'Compartidos', 'Intención', 'Tiempo prom. (seg)'], [34, 15, 15, 14, 12, 17, 13, 14, 12, 18]);
23359      categoryRows.forEach((item, index) => {
23360        const row = 5 + index;
23361        categorySheet.getCell(row, 1).value = item.label;
23362        categorySheet.getCell(row, 2).value = Number(item.published || 0);
23363        categorySheet.getCell(row, 3).value = Number(item.viewsNumber || 0);
23364        categorySheet.getCell(row, 4).value = { formula: `IFERROR(C${row}/B${row},0)`, result: Number(item.viewsPerRpNumber || 0) };
23365        categorySheet.getCell(row, 5).value = Number(item.videosNumber || 0);
23366        categorySheet.getCell(row, 6).value = { formula: `IFERROR(E${row}/C${row},0)`, result: Number(item.viewsNumber ?item.videosNumber / item.viewsNumber : 0) };
23367        categorySheet.getCell(row, 7).value = Number(item.contactsNumber || 0);
23368        categorySheet.getCell(row, 8).value = Number(item.sharesNumber || 0);
23369        categorySheet.getCell(row, 9).value = { formula: `G${row}+H${row}`, result: Number(item.intentNumber || 0) };
23370        categorySheet.getCell(row, 10).value = Number(item.avgTimeMs || 0) / 1000;
23371      });
23372      finalizeDataSheet(categorySheet, categoryRows.length, 10, { 2: '#,##0', 3: '#,##0', 4: '0.0', 5: '#,##0', 6: '0.0%', 7: '#,##0', 8: '#,##0', 9: '#,##0', 10: '0.0' });
23373      const rpSheet = prepareDataSheet('Ranking RP', 'Actividad por reproductor', snapshot.title, ['RP', 'Lote', 'Categoría', 'Vistas lote', 'Vistas RP', 'Videos', 'Activación', 'Imágenes', 'Contactos', 'Compartidos', 'Intención', 'Tiempo total (seg)', 'Tiempo prom. (seg)', 'Muestras'], [14, 18, 34, 13, 13, 11, 14, 12, 13, 14, 12, 18, 18, 12]);
23374      snapshot.rpActivityRows.forEach((item, index) => {
23375        const row = 5 + index;
23376        const values = [item.rp, item.lot, item.category, item.lotViewsNumber, item.rpViewsNumber, item.videosNumber, null, item.imagesNumber, item.contactsNumber, item.sharesNumber, null, item.durationTotal / 1000, item.durationAverage / 1000, item.samples];
23377        values.forEach((value, col) => { if (value !== null) rpSheet.getCell(row, col + 1).value = value; });
23378        rpSheet.getCell(row, 7).value = { formula: `IFERROR(F${row}/E${row},0)`, result: item.rpViewsNumber ?item.videosNumber / item.rpViewsNumber : 0 };
23379        rpSheet.getCell(row, 11).value = { formula: `I${row}+J${row}`, result: item.contactsNumber + item.sharesNumber };
23380      });
23381      finalizeDataSheet(rpSheet, snapshot.rpActivityRows.length, 14, { 4: '#,##0', 5: '#,##0', 6: '#,##0', 7: '0.0%', 8: '#,##0', 9: '#,##0', 10: '#,##0', 11: '#,##0', 12: '#,##0.0', 13: '0.0', 14: '#,##0' });
23382      const trendSheet = prepareDataSheet('Evolución', 'Evolución diaria del interés', snapshot.title, ['Fecha', 'Aperturas y evaluaciones', 'Intención'], [20, 26, 18]);
23383      snapshot.trend.forEach((item, index) => {
23384        const row = 5 + index;
23385        trendSheet.getCell(row, 1).value = /^\d{4}-\d{2}-\d{2}$/.test(item.dayKey) ?new Date(item.dayKey + 'T12:00:00') :item.label;
23386        trendSheet.getCell(row, 2).value = item.valueNumber;
23387        trendSheet.getCell(row, 3).value = item.intentNumber;
23388      });
23389      finalizeDataSheet(trendSheet, snapshot.trend.length, 3, { 1: 'dd/mm/yyyy', 2: '#,##0', 3: '#,##0' });
23390      const buffer = await workbook.xlsx.writeBuffer();
23391      this.downloadCabanaBlob(new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), this.cabanaExportFileBase(current) + '-estadisticas.xlsx');
23392      this.showToast('Informe de estadísticas exportado en Excel.');
23393    } catch (error) {
23394      console.error('[Cabanas] No se pudo generar el Excel de estadísticas', error);
23395      this.showToast(error?.message || 'No se pudo generar el Excel.');
23396    } finally {
23397      this.setState({ cabanaStatsExportBusy: '' });
23398    }
23399  }
23400  async createCabanaStatsStoryBlob(snapshot) {
23401    const current = this.state.currentCabana || {};
23402    const participants = this.cabanaRemateParticipants(current);
23403    const mainParticipant = participants.find((item) => item.type === 'principal') || participants[0] || {};
23404    const mainLogoSource = mainParticipant.logoDataUrl || mainParticipant.logoUrl || mainParticipant.logo || mainParticipant.logoWhiteDataUrl || '';
23405    const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || '';
23406    const consignatariaName = current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Rosgan';
23407    const consignatariaLogoSource = this.consWhiteLogo(consignatariaRaw || consignatariaName) || this.consLogo(consignatariaRaw || consignatariaName) || '';
23408    const [rosganLogo, mainLogo, consignatariaLogo, faviconLogo] = await Promise.all([
23409      this.loadCabanaCanvasImage('/assets/rosgan-logo-blue.png'),
23410      this.loadCabanaCanvasImage(mainLogoSource),
23411      this.loadCabanaCanvasImage(consignatariaLogoSource),
23412      this.loadCabanaCanvasImage('/favicon.png'),
23413    ]);
23414    const canvas = document.createElement('canvas');
23415    canvas.width = 1080;
23416    canvas.height = 1920;
23417    const ctx = canvas.getContext('2d');
23418    const dark = snapshot.dark || '#002e5f';
23419    const accent = snapshot.accent || '#0a5ca8';
23420    const background = ctx.createLinearGradient(0, 0, 1080, 1920);
23421    background.addColorStop(0, dark);
23422    background.addColorStop(.42, accent);
23423    background.addColorStop(1, '#031a31');
23424    ctx.fillStyle = background;
23425    ctx.fillRect(0, 0, 1080, 1920);
23426    ctx.fillStyle = 'rgba(255,255,255,.05)';
23427    ctx.beginPath(); ctx.arc(930, 150, 360, 0, Math.PI * 2); ctx.fill();
23428    ctx.beginPath(); ctx.arc(110, 1770, 430, 0, Math.PI * 2); ctx.fill();
23429    ctx.fillStyle = 'rgba(255,255,255,.96)';
23430    this.drawCabanaRoundRect(ctx, 54, 54, 972, 1812, 40); ctx.fill();
23431    const topGradient = ctx.createLinearGradient(54, 54, 1026, 680);
23432    topGradient.addColorStop(0, dark); topGradient.addColorStop(1, accent);
23433    ctx.fillStyle = topGradient;
23434    this.drawCabanaRoundRect(ctx, 54, 54, 972, 700, 40); ctx.fill();
23435    ctx.save(); ctx.beginPath(); this.drawCabanaRoundRect(ctx, 54, 54, 972, 700, 40); ctx.clip();
23436    ctx.fillStyle = 'rgba(255,255,255,.06)'; ctx.beginPath(); ctx.arc(930, 80, 350, 0, Math.PI * 2); ctx.fill();
23436 ctx.restore();
23437    ctx.fillStyle = 'rgba(255,255,255,.96)'; this.drawCabanaRoundRect(ctx, 78, 76, 278, 104, 22); ctx.fill(); this.drawCabanaRoundRect(ctx, 638, 76, 360, 104, 22); ctx.fill();
23438    if (rosganLogo) this.drawCabanaImageContain(ctx, rosganLogo, 96, 92, 242, 72);
23439    else { ctx.fillStyle = dark; ctx.font = '900 34px Archivo, Arial, sans-serif'; ctx.fillText('ROSGAN', 102, 142); }
23440    if (mainLogo) this.drawCabanaImageContain(ctx, mainLogo, 664, 86, 308, 84);
23441    ctx.fillStyle = 'rgba(255,255,255,.72)'; ctx.font = '900 17px Archivo, Arial, sans-serif'; ctx.letterSpacing = '3px'; ctx.fillText('INTELIGENCIA COMERCIAL', 86, 222); ctx.letterSpacing = '0px';
23442    ctx.fillStyle = '#fff'; ctx.font = '900 54px Archivo, Arial, sans-serif'; ctx.fillText('ESTADÍSTICAS', 86, 292); ctx.font = '900 49px Archivo, Arial, sans-serif'; ctx.fillText('DEL REMATE', 86, 346);
23443    ctx.fillStyle = 'rgba(255,255,255,.84)'; ctx.font = '800 22px Archivo, Arial, sans-serif'; this.wrapCabanaCanvasText(ctx, snapshot.title, 86, 391, 875, 28, 2);
23444    ctx.fillStyle = 'rgba(255,255,255,.12)'; this.drawCabanaRoundRect(ctx, 82, 454, 916, 82, 20); ctx.fill();
23445    if (consignatariaLogo) this.drawCabanaImageTintContain(ctx, consignatariaLogo, 110, 467, 96, 56);
23446    ctx.fillStyle = 'rgba(255,255,255,.68)'; ctx.font = '900 12px Archivo, Arial, sans-serif'; ctx.fillText('CONSIGNATARIA', 228, 483);
23447    ctx.fillStyle = '#fff'; ctx.font = '900 22px Archivo, Arial, sans-serif'; ctx.fillText(consignatariaName, 228, 514);
23448    const formatNumber = (value) => Number(value || 0).toLocaleString('es-AR');
23449    const storyKpis = [
23450      ['APERTURAS DE LOTE', formatNumber(snapshot.totals.lotViews)],
23451      ['EVALUACIONES DE RP', formatNumber(snapshot.totals.rpViews)],
23452      ['VIDEOS REPRODUCIDOS', formatNumber(snapshot.totals.videoPlays)],
23453      ['CONTACTOS', formatNumber(snapshot.totals.contacts)],
23454    ];
23455    storyKpis.forEach((item, index) => {
23456      const col = index % 2; const row = Math.floor(index / 2);
23457      const x = 82 + col * 462; const y = 566 + row * 92;
23458      ctx.fillStyle = 'rgba(255,255,255,.11)'; this.drawCabanaRoundRect(ctx, x, y, 442, 76, 17); ctx.fill();
23459      ctx.fillStyle = 'rgba(255,255,255,.7)'; ctx.font = '900 11px Archivo, Arial, sans-serif'; ctx.fillText(item[0], x + 20, y + 24);
23460      ctx.fillStyle = '#fff'; ctx.font = '900 30px Archivo, Arial, sans-serif'; ctx.fillText(item[1], x + 20, y + 58);
23461    });
23462    const panel = (x, y, width, height) => { ctx.save(); ctx.shadowColor = 'rgba(0,35,70,.12)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 7; ctx.fillStyle = '#fff'; this.drawCabanaRoundRect(ctx, x, y, width, height, 25); ctx.fill(); ctx.restore(); ctx.strokeStyle = 'rgba(0,46,95,.10)'; ctx.lineWidth = 2; this.drawCabanaRoundRect(ctx, x, y, width, height, 25); ctx.stroke(); };
23463    panel(82, 786, 916, 218);
23464    ctx.fillStyle = accent; ctx.font = '900 13px Archivo, Arial, sans-serif'; ctx.fillText('PULSO COMERCIAL', 112, 825);
23465    ctx.fillStyle = '#102f50'; ctx.font = '900 25px Archivo, Arial, sans-serif'; ctx.fillText('Cómo responde la demanda', 112, 862);
23466    const pulse = [
23467      ['CONVERSIÓN', snapshot.contactRateNumber.toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '%'],
23468      ['ACTIVACIÓN VIDEO', snapshot.videoRateNumber.toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '%'],
23469      ['TIEMPO PROMEDIO', this.formatCabanaVideoTime(snapshot.attentionSeconds)],
23470    ];
23471    pulse.forEach((item, index) => {
23472      const x = 112 + index * 286;
23473      ctx.fillStyle = '#edf5fb'; this.drawCabanaRoundRect(ctx, x, 890, 264, 82, 16); ctx.fill();
23474      ctx.fillStyle = '#71849a'; ctx.font = '900 10px Archivo, Arial, sans-serif'; ctx.fillText(item[0], x + 16, 916);
23475      ctx.fillStyle = dark; ctx.font = '900 27px Archivo, Arial, sans-serif'; ctx.fillText(item[1], x + 16, 955);
23476    });
23477    panel(82, 1030, 916, 322);
23478    ctx.fillStyle = accent; ctx.font = '900 13px Archivo, Arial, sans-serif'; ctx.fillText('DEMANDA POR SEGMENTO', 112, 1068);
23479    ctx.fillStyle = '#102f50'; ctx.font = '900 25px Archivo, Arial, sans-serif'; ctx.fillText('Categorías con mayor interés', 112, 1104);
23480    const categoryRows = (snapshot.categoryPerformance.length ?snapshot.categoryPerformance :snapshot.categories).slice(0, 3);
23481    const maxCategory = Math.max(1, ...categoryRows.map((item) => Number(item.viewsPerRpNumber || item.published || 0)));
23482    categoryRows.forEach((item, index) => {
23483      const y = 1144 + index * 61;
23484      ctx.fillStyle = '#183a5d'; ctx.font = '900 15px Archivo, Arial, sans-serif'; const label = String(item.label || 'Sin categoría'); ctx.fillText(label.length > 43 ?label.slice(0, 40) + '…' :label, 112, y + 18);
23485      const score = Number(item.viewsPerRpNumber || item.published || 0);
23486      ctx.fillStyle = '#e7eff6'; this.drawCabanaRoundRect(ctx, 112, y + 31, 690, 12, 6); ctx.fill();
23487      ctx.fillStyle = accent; this.drawCabanaRoundRect(ctx, 112, y + 31, Math.max(24, 690 * score / maxCategory), 12, 6); ctx.fill();
23488      ctx.textAlign = 'right'; ctx.fillStyle = '#60768c'; ctx.font = '900 14px Archivo, Arial, sans-serif'; ctx.fillText(item.viewsPerRpNumber != null ?(Number(item.viewsPerRpNumber).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + ' vistas/RP') :(formatNumber(item.published) + ' RPs'), 962, y + 35); ctx.textAlign = 'left';
23489    });
23490    panel(82, 1378, 916, 354);
23491    ctx.fillStyle = accent; ctx.font = '900 13px Archivo, Arial, sans-serif'; ctx.fillText('OPORTUNIDADES PRIORITARIAS', 112, 1417);
23492    ctx.fillStyle = '#102f50'; ctx.font = '900 25px Archivo, Arial, sans-serif'; ctx.fillText('RPs que concentran atención', 112, 1453);
23493    const leaders = snapshot.opportunities.slice(0, 3);
23494    leaders.forEach((item, index) => {
23495      const y = 1490 + index * 72;
23496      ctx.fillStyle = index === 0 ?accent :'#eaf2f8'; this.drawCabanaRoundRect(ctx, 112, y, 42, 42, 12); ctx.fill();
23497      ctx.fillStyle = index === 0 ?'#fff' :dark; ctx.font = '900 16px Archivo, Arial, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(String(index + 1), 133, y + 27); ctx.textAlign = 'left';
23498      ctx.fillStyle = '#102f50'; ctx.font = '900 17px Archivo, Arial, sans-serif'; ctx.fillText(`${item.rp} · ${item.lot}`, 174, y + 19);
23499      ctx.fillStyle = '#71849a'; ctx.font = '800 12px Archivo, Arial, sans-serif'; ctx.fillText(String(item.category || '').slice(0, 54), 174, y + 39);
23500      ctx.textAlign = 'right'; ctx.fillStyle = dark; ctx.font = '900 16px Archivo, Arial, sans-serif'; ctx.fillText(`${formatNumber(item.rpViewsNumber)} vistas · ${formatNumber(item.contactsNumber + item.sharesNumber)} intención`, 962, y + 27); ctx.textAlign = 'left';
23501      if (index < leaders.length - 1) { ctx.strokeStyle = '#e3ebf2'; ctx.beginPath(); ctx.moveTo(112, y + 57); ctx.lineTo(962, y + 57); ctx.stroke(); }
23502    });
23503    ctx.fillStyle = '#eef5fa'; this.drawCabanaRoundRect(ctx, 82, 1760, 916, 58, 16); ctx.fill();
23504    ctx.fillStyle = '#506a82'; ctx.font = '800 13px Archivo, Arial, sans-serif'; const insight = snapshot.insights[0]?.title || 'Datos acumulados del remate'; this.wrapCabanaCanvasText(ctx, insight, 108, 1785, 846, 18, 2);
23505    ctx.fillStyle = '#17304d'; ctx.font = '900 28px Archivo, Arial, sans-serif'; const footer = 'rosgan.app'; const footerWidth = ctx.measureText(footer).width; const iconSize = faviconLogo ?42 :0; const footerX = 540 - (footerWidth + iconSize + (iconSize ?12 :0)) / 2; if (faviconLogo) this.drawCabanaImageContain(ctx, faviconLogo, footerX, 1833, iconSize, iconSize); ctx.fillText(footer, footerX + iconSize + (iconSize ?12 :0), 1865);
23506    return await new Promise((resolve, reject) => {
23507      try { canvas.toBlob((blob) => blob ?resolve(blob) :reject(new Error('canvas-empty')), 'image/png', .94); }
23508      catch (error) { reject(error); }
23509    });
23510  }
23511  async downloadCabanaStatsStory() {
23512    if (this.state.cabanaStatsExportBusy) return;
23513    const snapshot = this.cabanaStatsExportSnapshot();
23514    if (!snapshot) { this.showToast('Todavía no hay estadísticas para exportar.'); return; }
23515    this.setState({ cabanaStatsExportBusy: 'story' });
23516    try {
23517      const blob = await this.createCabanaStatsStoryBlob(snapshot);
23518      this.downloadCabanaBlob(blob, this.cabanaExportFileBase(this.state.currentCabana || {}) + '-estadisticas-story-9x16.png');
23519      this.showToast('Story de estadísticas descargada.');
23520    } catch (error) {
23521      console.error('[Cabanas] No se pudo generar la story de estadísticas', error);
23522      this.showToast(error?.message || 'No se pudo generar la Story.');
23523    } finally {
23524      this.setState({ cabanaStatsExportBusy: '' });
23525    }
23526  }
23527  cabanaCatalogLocked(item = this.state.currentCabana) {
23528    return item?.visualization?.requerirRegistro === true && (!this.state.user || this.state.user.isAnonymous === true);
23529  }
23530  loginForCabanaCatalog() {
23531    this._initialPathname = window.location.pathname;
23532    this._initialSearch = window.location.search || '';
23533    this._initialPathRoute = 'cabanaDetail';
23534    this.setState({route:'login',routeLoading:false,authMode:'signin',authReady:true,publicAdvanceMode:'',authMessage:'Iniciá sesión o creá tu cuenta para ver los lotes de este remate.'});
23535  }
23536  async fetchCabanaCatalogPrices(item = this.state.currentCabana) {
23537    const id=this.cabanaRemateId(item);if(!id || this._cabanaCatalogPricesBusy)return;
23538    this._cabanaCatalogPricesBusy=true;
23539    try{const data=await this.cabanaApiFetch('/api/cabana-prebid-prices/'+encodeURIComponent(id),{cache:'no-store'});
23540      if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCatalogPrices:{id,prices:data.prices||{}}});
23541    }catch(error){console.warn('No se pudieron actualizar los importes del catálogo.');}
23542    finally{this._cabanaCatalogPricesBusy=false;}
23543  }
23544  async fetchCabanaLots(item) {
23545    if (this.cabanaCatalogLocked(item)) {
23546      this.setState({cabanaLots:[],cabanaLotsLoading:false,cabanaPublicDeepLoading:false});
23547      if (this.state.route === 'cabanaDetail' && this.cabanaRemateId(this.state.currentCabana) === this.cabanaRemateId(item)) this.loginForCabanaCatalog();
23548      return [];
23549    }
23550    const requestedPath = window.location.pathname;
23551    const id = this.cabanaRemateId(item);
23552    if (!id) { this.setState({ cabanaLots: [], cabanaLotsLoading: false }); return []; }
23553    this.setState({ cabanaLotsLoading: true });
23554    try {
23555      this.fetchCabanaCatalogPrices(item);
23556      // Fetch independently, but keep installment terms ready before displaying prices.
23557      const [, data] = await Promise.all([
23558        this.loadCabanaInstallments(item),
23559        this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(id) + '/lots/'),
23560      ]);
23561      const groups = this.listFrom(data, ['data', 'items', 'lots', 'lotes', 'groups']);
23562      const lots = this.normalizeCabanaAuctionLots(groups, item);
23563      if (window.location.pathname !== requestedPath || this.cabanaRemateId(this.state.currentCabana) !== id) return lots;
23564      // Lot responses must not replace newer auction details (participants and logos).
23565      this.setState({ cabanaLots: lots, cabanaLotsLoading: false, cabanasError: '' }, () => {
23566        this.hydrateVideoThumbnails(lots.flatMap((lot) => this.cabanaLotVideos(lot)));
23567      });
23568      return lots;
23569    } catch (error) {
23570      if (window.location.pathname !== requestedPath) return [];
23571      console.error('[Cabanas] No se pudieron cargar los lotes del remate', id, error);
23572      const lots = Array.isArray(this.state.cabanaLots) ?this.state.cabanaLots : [];
23573      this.setState({
23574        cabanaLots: lots,
23575        currentCabana: item,
23576        cabanaLotsLoading: false,
23577        cabanasError: error?.message || 'No se pudieron cargar los lotes del remate.',
23578      });
23579      return lots;
23580    }
23581  }
23582  normalizeCabanaAuctionLots(groups, remate) {
23583    const participants = this.cabanaRemateParticipants(remate);
23584    const participantById = new Map(participants.map((cabana) => [String(cabana.id || cabana.name || ''), cabana]));
23585    const flat = [];
23586    (Array.isArray(groups) ?groups : []).forEach((group, groupIndex) => {
23587      const groupItems = Array.isArray(group?.items) ?group.items : (Array.isArray(group?.lots) ?group.lots : (Array.isArray(group?.lotes) ?group.lotes : (group && typeof group === 'object' ?[group] : [])));
23588      const cabanaRef = group?.cabanaId || group?.cabana?.id || group?.cabanaName || group?.cabanaNombre || group?.cabana || '';
23589      const cabana = participantById.get(String(cabanaRef || '')) || participants.find((item) => this.consKey(item.name) === this.consKey(cabanaRef)) || null;
23590      groupItems.forEach((item, itemIndex) => {
23591        const values = item?.values && typeof item.values === 'object' ?item.values : {};
23592        const subformValues = {};
23593        (Array.isArray(item?.subforms) ?item.subforms : []).forEac
23593h((subform) => {
23594          const key = String(subform?.formKey || subform?.key || subform?.name || '').trim();
23595          if (key) {
23596            const values = subform?.values || {};
23597            subformValues[key] = values;
23598            subformValues[this.consKey(key)] = values;
23599            subformValues[this.consKey(key).replace(/-/g, '_')] = values;
23600          }
23601        });
23602        const mergedValues = { ...values };
23603        const primary = values.caravana || values.rp || values.lote || item?.lotCert || group?.lotCert || item?.id || (flat.length + 1);
23604        const itemCoverImage = item?.coverImage || item?.cover_image || null;
23605        const coverMedia = typeof itemCoverImage === 'string' ?{ url: itemCoverImage } : (itemCoverImage && typeof itemCoverImage === 'object' ?itemCoverImage : null);
23606        const groupCoverImage = group?.coverImage || group?.cover_image || null;
23607        const lotCoverMedia = typeof groupCoverImage === 'string' ?{ url: groupCoverImage } : (groupCoverImage && typeof groupCoverImage === 'object' ?groupCoverImage : null);
23608        const images = [
23609          ...(coverMedia?.url || coverMedia?.downloadUrl ?[coverMedia] : []),
23610          ...this.listFrom(item, ['images', 'imagenes', 'photos', 'fotos', 'media']).map((media) => typeof media === 'string' ?{ url: media } : media),
23611        ];
23612        const videos = this.listFrom(item, ['videos']).map((media) => typeof media === 'string' ?{ url: media } : media);
23613        flat.push({
23614          ...item,
23615          id: group?.lotCert || group?.id || item?.lotCert || item?.id || (groupIndex + '-' + itemIndex),
23616          itemId: item?.id || (groupItems.length === 1 ?item?.cert : null),
23617          groupId: group?.id || group?.lotCert || null,
23618          groupDisplayOrder: Number(group?.displayOrder || groupIndex + 1),
23619          itemDisplayOrder: Number(item?.itemDisplayOrder || item?.rpDisplayOrder || item?.displayOrder || itemIndex + 1),
23620          commercial: item?.commercial || item?.comercial || null,
23621          groupItemCount: groupItems.length,
23622          prebidBaseOverride: group.prebidBaseOverride ?? item.prebidBaseOverride ?? null,
23623          prebidIncrementOverride: group.prebidIncrementOverride ?? item.prebidIncrementOverride ?? null,
23624          effectivePrebidBase: group.effectivePrebidBase ?? item.effectivePrebidBase ?? null,
23625          effectivePrebidIncrement: group.effectivePrebidIncrement ?? item.effectivePrebidIncrement ?? null,
23626          remateId: this.cabanaRemateId(remate),
23627          lotCert: group?.lotCert || item?.lotCert || group?.id || item?.id,
23628          categoryId: group?.categoryId || item?.categoryId || null,
23629          categoryDisplayOrder: this.cabanaCategoryDisplayOrder(group, this.cabanaCategoryDisplayOrder(item, groupIndex + 1)),
23630          categoryOrder: this.cabanaCategoryDisplayOrder(group, this.cabanaCategoryDisplayOrder(item, groupIndex + 1)),
23631          categoryName: group?.categoryName || item?.categoryName || 'Categoría',
23632          cabanaId: group?.cabanaId || item?.cabanaId || cabana?.id || null,
23633          cabanaNombre: group?.cabanaName || item?.cabanaName || cabana?.name || '',
23634          cabanaLogoDataUrl: cabana?.logoDataUrl || '',
23635          cabanaLogoWhiteDataUrl: cabana?.logoWhiteDataUrl || '',
23636          cabanaBrandColor: cabana?.brandColor || '',
23637          identifier: primary,
23638          values: mergedValues,
23639          subformValues,
23640          displayOrder: Number(group?.displayOrder || item?.displayOrder || groupIndex + 1),
23641          rp: values.rp || values.RP || primary,
23642          hba: values.hba,
23643          caravana: values.caravana,
23644          padre: values.padre,
23645          nacimiento: values.fecha_de_nacimiento,
23646          peso: values.peso_actual || values.peso || values.pesoPromedio,
23647          ce: values.ce,
23648          images,
23649          videos,
23650          coverImage: coverMedia,
23651          lotCoverImage: lotCoverMedia,
23652          thumbnailUrl: item?.thumbnailUrl || item?.coverImageUrl || coverMedia?.url || coverMedia?.downloadUrl || images[0]?.url || images[0]?.downloadUrl || '',
23653          lotThumbnailUrl: group?.thumbnailUrl || group?.coverImageUrl || lotCoverMedia?.url || lotCoverMedia?.downloadUrl || '',
23654          __videosLoaded: true,
23655          updatedAt: item?.updatedAt || group?.updatedAt || remate?.updatedAt || '',
23656        });
23657      });
23658    });
23659    return this.cabanaSortLotsByCategoryOrder(flat);
23660  }
23661  openCabanaLot(lot, variants = null) {
23662    if (this.cabanaCatalogLocked()) { this.loginForCabanaCatalog(); return; }
23663    const lotId = this.cabanaLotId(lot);
23664    const selectedLot = { ...lot, videos: Array.isArray(lot?.videos) ?lot.videos : [], __videosLoaded: true };
23665    this.syncLacassinaLotPath(lot);
23666    if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(this.state.currentCabana, 'lotes/' + encodeURIComponent(this.cabanaPrebidCert(lot) || this.cabanaLotId(lot)));
23667    this.trackCabanaAnalyticsOnce('lot_view', lot, { metadata: { action: 'open_lot' } });
23668    this.trackCabanaAnalyticsOnce('rp_view', lot, { metadata: { action: 'open_visible_rp' } });
23669    this.startCabanaLotViewTimer(lot);
23670    this.setState({
23671      route: 'cabanaLotDetail',
23672      currentCabanaLot: selectedLot,
23673      currentCabanaLotVariants: Array.isArray(variants) && variants.length ?variants : [lot],
23674      cbLotVideo: 0,
23675      cabanaLotVideosLoading: false,
23676      cabanaLotMediaReady: false,
23677      cabanaLotMediaFailed: false,
23678      cabanaLotVideoPlaying: false,
23679      cabanaLotVideoProgress: 0,
23680      cabanaLotVideoDuration: 0,
23681      cabanaLotVideoVolume: 0,
23682      cabanaLotVideoMuted: true,
23683      cabanaPrebids: [],
23684      cabanaPrebidMine: [],
23685      cabanaPrebidsError: '',
23686      cabanaPublicDeepLoading: false,
23687    });
23688    window.scrollTo(0, 0);
23689    this.syncCabanaLotVideo(lotId);
23690    this.fetchCabanaPrebids(lot);
23691  }
23692  cabanaPrebidEnabled(remate = this.state.currentCabana) {
23693    return !!(remate?.prebid?.habilitarPreofertas || remate?.prebid?.habilitada || remate?.prebid?.enabled || remate?.prebid?.isActive || String(remate?.preoferta || '').toLowerCase() === 'activa');
23694  }
23695  canDeleteCabanaPrebid() {
23696    return !!this.state.user && this.apiRoleIds(this.state.apiUser).some(role=>['admin','sysadmin'].includes(role));
23697  }
23698  async deleteCabanaPrebid(bid) {
23699    if(!this.canDeleteCabanaPrebid()){this.showToast('No tenés permisos para eliminar preofertas');return;}
23700    if(this.state.cabanaPrebidDeleting)return;
23701    const remate=this.state.currentCabana,lot=this.state.currentCabanaLot;
23702    const auctionId=this.cabanaRemateId(remate),lotCert=bid.lotCert || this.cabanaPrebidCert(lot);
23703    if(!auctionId || !lotCert || !bid.id)return;
23704    if(!window.confirm('¿Eliminar esta preoferta? El sistema recalculará la oferta ganadora del lote.'))return;
23705    this.setState({cabanaPrebidDeleting:true});
23706    try {
23707      await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(auctionId)+'/lots/'+encodeURIComponent(lotCert)+'/prebids/'+encodeURIComponent(bid.id),{method:'DELETE'});
23708    }catch(error){
23709      this.showToast(error.status===403?'No tenés permisos para eliminar preofertas':error.status===404?'L
23709a preoferta no existe o ya fue eliminada':error.status===400?(error.data?.detail || 'No se pudo eliminar la preoferta'):'No se pudo eliminar la preoferta');
23710      this.setState({cabanaPrebidDeleting:false});return;
23711    }
23712    try {
23713      if(this.state.currentCabana===remate){
23714        if(this.state.route==='cabanaPrebidPanel')await this.fetchCabanaPrebidPanel(remate);
23715        if(lot && this.state.currentCabanaLot===lot)await this.fetchCabanaPrebids(lot);
23716      }
23717      this.showToast('Preoferta eliminada correctamente');
23718    }finally{this.setState({cabanaPrebidDeleting:false});}
23719  }
23720  async resolveCabanaPrebid(bid, action) {
23721    if (this.state.cabanaPrebidApprovalBusy || !['approve','reject'].includes(action)) return;
23722    const remate=this.state.currentCabana;
23723    this.setState({cabanaPrebidApprovalBusy:true});
23724    try {
23725      await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(this.cabanaRemateId(remate))+'/lots/'+encodeURIComponent(bid.lotCert)+'/prebids/'+encodeURIComponent(bid.id)+'/'+action,{method:'POST'});
23726      await this.fetchCabanaPrebidPanel(remate);
23727      if (this.state.currentCabanaLot) await this.fetchCabanaPrebids();
23728      this.showToast(action === 'approve' ?'Preoferta aprobada.':'Preoferta rechazada.');
23729    } catch(error) {this.showToast(error.status === 403 ?'No sos aprobador asignado ni administrador de este remate.':error?.data?.detail || error.message);}
23730    finally {this.setState({cabanaPrebidApprovalBusy:false});}
23731  }
23732  cabanaPrebidGrouped(remate = this.state.currentCabana) {
23733    return remate?.prebid?.agruparPreofertas === true;
23734  }
23735  cabanaPrebidNumber(value) {
23736    if (typeof value === 'number') return Number.isFinite(value) ?value : 0;
23737    const text = String(value ?? '').trim().replace(/\s/g, '').replace(/\$/g, '');
23738    if (!text) return 0;
23739    const looksLikeThousands = /^-?\d{1,3}(\.\d{3})+$/.test(text);
23740    const normalized = text.includes(',') ?text.replace(/\./g, '').replace(',', '.') : (looksLikeThousands ?text.replace(/\./g, '') : text.replace(/[^\d.-]/g, ''));
23741    const number = Number(normalized);
23742    return Number.isFinite(number) ?number : 0;
23743  }
23744  cabanaPrebidCert(lot) {
23745    return String(lot?.lotCert || lot?.groupId || lot?.lotId || lot?.id || '').trim();
23746  }
23747  cabanaPrebidAnimalCert(lot) {
23748    return String(lot?.cert || lot?.certificate || lot?.itemCert || lot?.values?.cert || lot?.values?.certificado || lot?.identifier || lot?.rp || lot?.values?.rp || '').trim();
23749  }
23750  cabanaPrebidBase(lot = this.state.currentCabanaLot) {
23751    return window.CabanaPrebid.pricing(lot,this.state.currentCabana).base;
23752  }
23753  cabanaPrebidIncrement(remate = this.state.currentCabana) {
23754    return window.CabanaPrebid.pricing(this.state.currentCabanaLot,remate).increment;
23755  }
23756  cabanaPrebidIdentifier(lot) {
23757    return window.CabanaPrebid.identifier(lot || {},this.cabanaRemateSchema(this.state.currentCabana));
23758  }
23759  cabanaPrebidMinimum(selected = this.state.cabanaPrebidForm?.selectedIdentifiers || []) {
23760    return window.CabanaPrebid.nextAmount(this.state.currentCabanaLot,this.state.currentCabana,this.state.cabanaPrebids,selected);
23761  }
23762  cabanaPrebidRows(data) {
23763    const root = data?.data ?? data;
23764    const raw = Array.isArray(root) ?root : (root?.prebids || root?.items || root?.results || root?.history || []);
23765    return (Array.isArray(raw) ?raw : []).map((item, index) => {
23766      const status = String(item?.status || item?.estado || 'Pendiente');
23767      const certs = Array.isArray(item?.certs) ?item.certs : (Array.isArray(item?.selectedCerts) ?item.selectedCerts : []);
23768      return {
23769        ...item,
23770        targets: Array.isArray(item?.targets) ?item.targets : [],
23771        id: String(item?.id || item?.prebidId || index),
23772        amount: this.cabanaPrebidNumber(item?.amount ?? item?.monto),
23773        status,
23774        isWinner: Array.isArray(item?.targets) ?item.targets.some(t => t.isWinning === true) : /ganadora|winner|winning/i.test(status),
23775        certs: certs.map(String),
23776        cuitCuil: String(item?.cuitCuil || item?.cuit || item?.cuil || ''),
23777        buyerUid: String(item?.buyerUid || item?.userUid || item?.userId || item?.uid || item?.createdByUid || ''),
23778        buyerEmail: String(item?.buyerEmail || item?.userEmail || item?.email || ''),
23779        buyerName: String(item?.bidderName || item?.buyerName || item?.userName || item?.displayName || item?.nombre || ''),
23780        enComisionPara: String(item?.enComisionPara || item?.commissionFor || ''),
23781        createdAt: item?.createdAt || item?.fechaHora || item?.date || item?.timestamp || '',
23782      };
23783    }).sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
23784  }
23785  cabanaWinningPrebid(rows = this.state.cabanaPrebids) {
23786    const selected=this.state.cabanaPrebidModalOpen ?this.state.cabanaPrebidForm?.selectedIdentifiers || []:[this.cabanaPrebidIdentifier(this.state.currentCabanaLot).value];
23787    return (Array.isArray(rows) ?rows : []).filter(item => window.CabanaPrebid.accepted(item) && (this.cabanaPrebidGrouped() || window.CabanaPrebid.identifiers(item).some(id=>selected.includes(id)))).sort((a,b)=>b.amount-a.amount)[0] || null;
23788  }
23789  cabanaPrebidOwnStorageKey(auctionId, lotCert) {
23790    const uid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || '').trim();
23791    if (!uid || !auctionId || !lotCert) return '';
23792    return 'rosgan:cabana-prebids:mine:v1:' + encodeURIComponent(uid) + ':' + encodeURIComponent(String(auctionId)) + ':' + encodeURIComponent(String(lotCert));
23793  }
23794  cabanaPrebidLocalMine(auctionId, lotCert) {
23795    const key = this.cabanaPrebidOwnStorageKey(auctionId, lotCert);
23796    if (!key || typeof window === 'undefined' || !window.localStorage) return [];
23797    try {
23798      const stored = JSON.parse(window.localStorage.getItem(key) || '[]');
23799      return Array.isArray(stored) ?stored.filter((item) => this.cabanaPrebidNumber(item?.amount) > 0) : [];
23800    } catch (_) {
23801      return [];
23802    }
23803  }
23804  persistCabanaPrebidOwnOffer(auctionId, lotCert, offer) {
23805    const key = this.cabanaPrebidOwnStorageKey(auctionId, lotCert);
23806    if (!key || typeof window === 'undefined' || !window.localStorage || !offer) return;
23807    try {
23808      const current = this.cabanaPrebidLocalMine(auctionId, lotCert);
23809      const amount = this.cabanaPrebidNumber(offer.amount);
23810      const id = String(offer.id || '');
23811      const withoutDuplicate = current.filter((item) => {
23812        if (id && String(item?.id || '') === id) return false;
23813        return this.cabanaPrebidNumber(item?.amount) !== amount;
23814      });
23815      window.localStorage.setItem(key, JSON.stringify([...withoutDuplicate, { ...offer, amount }].slice(-50)));
23816    } catch (_) {}
23817  }
23818  cabanaPrebidPanelLots(data = this.state.cabanaPrebidPanelData) {
23819    const panelData = data || {};
23820    const items = Array.isArray(panelData.items) ?panelData.items : [];
23821    const providedLots = Array.isArray(panelData.lots) ?panelData.lots : [];
23822    const lotsByKey = new Map();
23823    const ensureLot = (source = {}) => {
23824      const key = String(source.lotCert || source.cert || source.id || '').trim();
23825      if (!key) return null;
23826      if (!lotsByKey.has(key)) lotsByKey.set(key, {
23827        key,
23828        label: source.lotLabel || source.label || source.name || ('Lote ' + key),
23829        category: source.category || source.categoryName || '',
23830        cabanaName: source.cabanaName || source.cabana?.name || '',
23831        offerCount: Number(source.offerCount || 0),
23832        currentAmountValue: Number(source.currentAmount || 0),
23833        bidderKeys: new Set(),
23834      });
23835      return lotsByKey.get(key);
23836    };
23837    providedLots.forEach(ensureLot);
23838    items.forEach((item) => {
23839      const lot = ensureLot(item);
23840      if (!lot) return;
23841      lot.label = item?.lotLabel || lot.label;
23842      lot.category = item?.category || lot.category;
23843      lot.cabanaName = item?.cabanaName || lot.cabanaName;
23844      lot.offerCount += providedLots.length ?0 : 1;
23845      if (item?.isCurrent) lot.currentAmountValue = Number(item?.amount || 0);
23846      const bidderKey = String(item?.buyer?.uid || item?.buyer?.email || '').trim();
23847      if (bidderKey) lot.bidderKeys.add(bidderKey);
23848    });
23849    return Array.from(lotsByKey.values()).map((lot) => ({
23850      ...lot,
23851      meta: [lot.category, lot.cabanaName].filter(Boolean).join(' · ') || 'Sin datos adicionales',
23852      currentAmount: lot.currentAmountValue > 0 ?this.fmtARS(lot.currentAmountValue) : 'Sin ofertas',
23853      offerLabel: lot.offerCount === 1 ?'1 preoferta' : lot.offerCount.toLocaleString('es-AR') + ' preofertas',
23854      bidderCount: lot.bidderKeys.size,
23855    })).sort((a, b) => String(a.label).localeCompare(String(b.label), 'es', { numeric: true, sensitivity: 'base' }));
23856  }
23857  syncCabanaPrebidPanelLotQuery(lotCert = this.state.cabanaPrebidPanelLot) {
23858    if (this.state.publicAdvanceMode || this.state.route !== 'cabanaPrebidPanel') return;
23859    const key = String(lotCert || '').trim();
23860    const base = this.cabanaPath(this.state.currentCabana, 'preofertas');
23861    const path = base + (key ?'?lote=' + encodeURIComponent(key) : '');
23862    if (window.location.pathname + window.location.search === path) return;
23863    try { window.history.replaceState({}, '', path); } catch {}
23864  }
23865  selectCabanaPrebidPanelLot(lotCert) {
23866    const key = String(lotCert || '').trim();
23867    if (!key || key === String(this.state.cabanaPrebidPanelLot || '')) return;
23868    this.setState({ cabanaPrebidPanelLot: key, cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all' }, () => {this.syncCabanaPrebidPanelLotQuery(key);this.fetchCabanaPrebidPanel();});
23869  }
23870  async checkCabanaPrebidSummary(remate = this.state.currentCabana) {
23871    const uid=this.state.user?.uid, id=this.cabanaRemateId(remate);
23872    this.setState({cabanaPrebidSummaryAccess:null});
23873    if(!uid || !id)return;
23874    try { const result=await this.cabanaApiFetch('/api/cabana-prebid-reports/'+encodeURIComponent(id)+'?access=1',{cache:'no-store'});
23875      if(this.state.user?.uid===uid && this.cabanaRemateId(this.state.currentCabana)===id && result.allowed)this.setState({cabanaPrebidSummaryAccess:{uid,id}});
23876    } catch {}
23877  }
23878  openCabanaPrebidSummary() {
23879    const id=this.cabanaRemateId(this.state.currentCabana);
23880    if(!id || !this.state.user)return;
23881    window.CabanaPrebidReport.open({
23882      identity:()=>this.state.user?.uid,
23883      load:()=>this.cabanaApiFetch('/api/cabana-prebid-reports/'+encodeURIComponent(id),{cache:'no-store'}),
23884      excelJs:()=>this.ensureCabanaExcelJs(), download:(blob,name)=>this.downloadCabanaBlob(blob,name)
23885    });
23886  }
23887  async fetchCabanaPrebidPanel(remate = this.state.currentCabana, options = {}) {
23888    const auctionId = this.cabanaRemateId(remate);
23889    if (!auctionId) return null;
23890    if (this._cabanaPrebidPanelRefreshBusy) {this._cabanaPrebidPanelReload = true;return null;}
23891    const requestSelection = String(this.state.cabanaPrebidPanelLot || '');
23892    const probe = options.probe === true;
23893    const silent = options.silent === true || probe;
23894    this._cabanaPrebidPanelRefreshBusy = true;
23895    if (!silent) this.setState({ cabanaPrebidPanelLoading: true, cabanaPrebidPanelError: '' });
23896    try {
23897      const uid=this.state.user?.uid;
23898      const base='/api/cabin-auctions/'+encodeURIComponent(auctionId);
23899      const pending=this.cabanaPrebidRows(await this.cabanaApiFetch(base+'/prebids/pending',{cache:'no-store'}));
23900      const response=await this.cabanaApiFetch(base+'/lots',{cache:'no-store'});
23901      const lots=this.normalizeCabanaAuctionLots(this.listFrom(response,['data','items','lots','lotes','groups']),remate);
23902      const lotOptions=[...new Map(lots.map(l => [this.cabanaPrebidCert(l),{lotCert:this.cabanaPrebidCert(l),lotLabel:'Lote '+this.cabanaLotNumber(l),categoryName:l.categoryName}])).values()];
23903      const requested=String(this.state.cabanaPrebidPanelLot || new URLSearchParams(window.location.search).get('lote') || pending[0]?.lotCert || lotOptions[0]?.lotCert || '');
23904      const history=requested ?this.cabanaPrebidRows(await this.cabanaApiFetch(base+'/lots/'+encodeURIComponent(requested)+'/prebids/manage',{cache:'no-store'})):[];
23905      if (this.state.user?.uid !== uid) return null;
23906      if (String(this.state.cabanaPrebidPanelLot || '') !== requestSelection) {this._cabanaPrebidPanelReload=true;return null;}
23907      const combined=[...new Map([...pending,...history].map(b => [b.id,b])).values()];
23908      const data={updatedAt:new Date().toISOString(),lots:lotOptions,pendingCount:pending.length,items:combined.map(b => ({...b,isCurrent:b.targets.some(t => t.isWinning),buyer:{uid:b.buyerUid,name:b.buyerName,email:b.buyerEmail,cuitCuil:b.cuitCuil},animals:b.targets.map(t => ({cert:t.characteristicIdentifierValue ?? t.cert,identifierName:t.characteristicIdentifierName || 'Cert.',fields:[{label:t.characteristicIdentifierName || 'Cert.',value:t.isWinning ?'Ganad
23908ora':b.status === 'Ganadora' ?'Superada':b.status}]}))}))};
23909      if (['cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel'].includes(this.state.route) && this.cabanaRemateId(this.state.currentCabana) === String(auctionId)) {
23910        const lotOptions = this.cabanaPrebidPanelLots(data);
23911        const queryLot = requested;
23912        const currentLot = queryLot || String(this.state.cabanaPrebidPanelLot || '');
23913        const selectedLot = lotOptions.some((lot) => lot.key === currentLot) ?currentLot : (lotOptions[0]?.key || '');
23914        this.setState({ cabanaPrebidPanelAllowed: true, cabanaPrebidPanelData: data || null, cabanaPrebidPanelLoading: false, cabanaPrebidPanelError: '', cabanaPrebidPanelLot: selectedLot }, () => this.syncCabanaPrebidPanelLotQuery(selectedLot));
23915      }
23916      return data;
23917    } catch (error) {
23918      const denied = error?.status === 401 || error?.status === 403;
23919      if (['cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel'].includes(this.state.route) && this.cabanaRemateId(this.state.currentCabana) === String(auctionId)) {
23920        const panelPageDenied = denied && this.state.route === 'cabanaPrebidPanel';
23921        if (panelPageDenied) this.syncCabanaLocation(remate, '', { replace: true });
23922        this.setState({
23923          route: panelPageDenied ?'cabanaDetail' : this.state.route,
23924          cabanaPrebidPanelAllowed: denied ?false : this.state.cabanaPrebidPanelAllowed,
23925          ...(denied ?{cabanaPrebidPanelData:null}:{}),
23926          cabanaPrebidPanelOpen: denied ?false : this.state.cabanaPrebidPanelOpen,
23927          cabanaPrebidPanelLoading: false,
23928          cabanaPrebidPanelError: denied || probe ?'' : (error?.data?.message || error?.message || 'No se pudo actualizar el panel de preofertas.'),
23929        });
23930        if (panelPageDenied) this.showToast('No tenés permiso para ver el panel privado de preofertas.');
23931      }
23932      return null;
23933    } finally {
23934      this._cabanaPrebidPanelRefreshBusy = false;
23935      if (this._cabanaPrebidPanelReload) {this._cabanaPrebidPanelReload=false;this.fetchCabanaPrebidPanel(this.state.currentCabana,{silent:true});}
23936    }
23937  }
23938  toggleCabanaPrebidPanel() {
23939    if (!this.state.cabanaPrebidPanelAllowed) return;
23940    this.openCabanaPrebidPanelPage(this.state.currentCabana);
23941  }
23942  openCabanaPrebidPanelPage(item = this.state.currentCabana, syncPath = true, requestedLotOverride = '') {
23943    item = this.normalizeCabanaAuctionRecord(item);
23944    if (!item) return;
23945    const requestedLot = syncPath ?'' : (String(requestedLotOverride || '').trim() || String(new URLSearchParams(window.location.search || '').get('lote') || '').trim());
23946    if (syncPath) this.syncCabanaLocation(item, 'preofertas');
23947    this.setState({
23948      route: 'cabanaPrebidPanel', currentCabana: item, cabanaPrebidPanelOpen: true,
23949      cabanaPrebidPanelLoading: true, cabanaPrebidPanelError: '', cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all', cabanaPrebidPanelLot: requestedLot,
23950    }, () => this.fetchCabanaPrebidPanel(item));
23951    this.ensureCabanaBrand(item);
23952    window.scrollTo(0, 0);
23953  }
23954  closeCabanaPrebidPanelPage() {
23955    const item = this.state.currentCabana;
23956    this.syncCabanaLocation(item);
23957    this.setState({ route: 'cabanaDetail', cabanaPrebidPanelOpen: false, cabanaPrebidPanelError: '' });
23958    window.scrollTo(0, 0);
23959  }
23960  syncCabanaPrebidEvents() {
23961    const active=this.state.cabanaPrebidModalOpen && this.state.route==='cabanaLotDetail';
23962    const key=active ? [this.cabanaRemateId(this.state.currentCabana),this.cabanaPrebidCert(this.state.currentCabanaLot)].map(encodeURIComponent).join('/') : '';
23963    if(this._cabanaPrebidEventKey===key)return;
23964    this._cabanaPrebidEvents?.close();this._cabanaPrebidEvents=null;this._cabanaPrebidEventKey=key;
23965    if(!key || typeof EventSource==='undefined')return;
23966    const source=new EventSource('/api/cabana-prebid-events/'+key);this._cabanaPrebidEvents=source;
23967    source.addEventListener('change',()=>{
23968      if(this._cabanaPrebidEvents!==source)return;
23969      this._cabanaPrebidRealtimePending=true;this.refreshCabanaPrebidRealtime();
23970    });
23971  }
23972  cabanaPrebidLiveForm(rows) {
23973    if(!this.state.cabanaPrebidModalOpen || this.state.cabanaPrebidSubmitting)return {};
23974    const form=this.state.cabanaPrebidForm || {};
23975    const minimum=window.CabanaPrebid.nextAmount(this.state.currentCabanaLot,this.state.currentCabana,rows,form.selectedIdentifiers || []);
23976    if(minimum==null || Number(form.amount)>=minimum)return {};
23977    return {cabanaPrebidForm:{...form,amount:minimum}};
23978  }
23979  refreshCabanaPrebidRealtime() {
23980    this.syncCabanaPrebidEvents();
23981    if (typeof document !== 'undefined' && document.hidden) return;
23982    if (this.state.route === 'cabanaLotDetail' && this.state.currentCabanaLot && this.cabanaPrebidEnabled() && !this.state.cabanaPrebidSubmitting && !this._cabanaPrebidRealtimeBusy) {
23983      this._cabanaPrebidRealtimeBusy = true;this._cabanaPrebidRealtimePending=false;
23984      Promise.resolve(this.fetchCabanaPrebids(this.state.currentCabanaLot, { loading: false }))
23985        .finally(() => { this._cabanaPrebidRealtimeBusy = false;if(this._cabanaPrebidRealtimePending)this.refreshCabanaPrebidRealtime(); });
23986    }
23987    if (this.state.route === 'cabanaDetail') this.fetchCabanaCatalogPrices();
23988    if (this.state.route === 'cabanaPrebidPanel' && this.state.cabanaPrebidPanelAllowed) {
23989      this.fetchCabanaPrebidPanel(this.state.currentCabana, { silent: true });
23990    }
23991  }
23992  async fetchCabanaPrebids(lot = this.state.currentCabanaLot, options = {}) {
23993    const apply = options.apply !== false;
23994    const showLoading = options.loading !== false;
23995    if (!lot || !this.cabanaPrebidEnabled()) {
23996      if (apply && (this.state.cabanaPrebids.length || this.state.cabanaPrebidsError)) this.setState({ cabanaPrebids: [], cabanaPrebidsError: '', cabanaPrebidsLoading: false });
23997      return [];
23998    }
23999    const auctionId = this.cabanaRemateId(this.state.currentCabana);
24000    const lotCert = this.cabanaPrebidCert(lot);
24001    if (!auctionId || !lotCert) return [];
24002    if (apply && showLoading) this.setState({ cabanaPrebidsLoading: true, cabanaPrebidsError: '' });
24003    const requestedUid=String(this.state.user?.uid || '');
24004    try {
24005      const [data, mineData] = await Promise.all([
24006        this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/prebids', { cache: 'no-store' }),
24007        this.apiFetch('/api/notifications/cabana-prebid/mine?auctionId=' + encodeURIComponent(auctionId) + '&lotCert=' + encodeURIComponent(lotCert), { cache: 'no-store' })
24008          .catch(() => ({ items: [] })),
24009      ]);
24010      if(String(this.state.user?.uid || '')!==requestedUid)return [];
24011      const currentUid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || '');
24012      const currentEmail = String(this.state.apiUser?.email || this.state.user?.email || '').trim().toLowerCase();
24013      const remoteMine = Array.isArray(mineData?.items) ?mineData.items : [];
24014      const localMine = this.cabanaPrebidLocalMine(auctionId, lotCert);
24015      const knownMine = [...localMine, ...remoteMine];
24016      const ownIds = new Set(knownMine.map((item) => String(item?.id || '')).filter(Boolean));
24017      const rows = this.cabanaPrebidRows(data).map((row) => ({
24018        ...row,
24019        isOwn: !!(
24020          (currentUid && row.buyerUid && row.buyerUid === currentUid)
24021          || (currentEmail && row.buyerEmail && row.buyerEmail.trim().toLowerCase() === currentEmail)
24022          || ownIds.has(String(row.id || ''))
24023        ),
24024      }));
24025      if (apply && this.cabanaPrebidCert(this.state.currentCabanaLot) === String(lotCert)) this.setState({ ...this.cabanaPrebidLiveForm(rows), cabanaPrebids: rows, cabanaPrebidMine: knownMine, cabanaPrebidsLoading: false, cabanaPrebidsError: '' });
24026      return rows;
24027    } catch (error) {
24028      const message = error?.data?.detail || error?.message || 'No se pudieron cargar las preofertas.';
24029      if (apply && this.cabanaPrebidCert(this.state.currentCabanaLot) === String(lotCert)) this.setState({ cabanaPrebids: [], cabanaPrebidsLoading: false, cabanaPrebidsError: message });
24030      return [];
24031    }
24032  }
24033  cabanaPrebidIdentifierLabel(value) {
24034    const variants = this.state.currentCabanaLotVariants?.length ?this.state.currentCabanaLotVariants : [this.state.currentCabanaLot];
24035    const identifier = variants.map(lot => this.cabanaPrebidIdentifier(lot)).find(item => String(item.value) === String(value));
24036    return [identifier?.name || 'Cert.', value].join(' ');
24037  }
24038  cabanaPrebidAvailableCerts() {
24039    const variants = Array.isArray(this.state.currentCabanaLotVariants) && this.state.currentCabanaLotVariants.length ?this.state.currentCabanaLotVariants : [this.state.currentCabanaLot];
24040    return [...new Set(variants.map((lot) => this.cabanaPrebidIdentifier(lot).value).filter(Boolean))];
24041  }
24042  async openCabanaPrebid() {
24043    if (!this.cabanaPrebidEnabled() || this._openingCabanaPrebid) return;
24044    const windowState = window.CabanaPrebid.windowState(this.state.currentCabana);
24045    if (!windowState.active) { this.showToast(windowState.upcoming ? 'Las preofertas todavía no comenzaron.' : 'Las preofertas están cerradas.'); return; }
24046    if (!this.state.user) { this.showToast('Iniciá sesión para preofertar.'); return; }
24047    const uid = this.state.user.uid, lot = this.state.currentCabanaLot;
24048    this._openingCabanaPrebid = true;
24049    try {
24050      await this.loadCabanaInstallments(this.state.currentCabana);
24051      const response = await this.cabanaApiFetch('/api/users/me');
24052      if (this.state.user?.uid !== uid || this.state.currentCabanaLot !== lot) return;
24053      const profile = response?.data ?? response;
24054      await this.fetchCabanaPrebids(lot);
24055      if (this.state.user?.uid !== uid || this.state.currentCabanaLot !== lot) return;
24056      const selectedIdentifiers = [this.cabanaPrebidIdentifier(lot).value || this.cabanaPrebidAvailableCert
24056s()[0]].filter(Boolean);
24057      const amount = this.cabanaPrebidMinimum(selectedIdentifiers);
24058      if (amount == null) { this.showToast('Este lote todavía no tiene base e incremento configurados.'); return; }
24059      this.setState({cabanaPrebidCuitCheck:null, cabanaPrebidProfile:profile, cabanaPrebidModalOpen:true, cabanaPrebidMessage:null, cabanaPrebidRolling:false,
24060        cabanaPrebidForm:{cuitCuil:'',amount,selectedIdentifiers,enComision:false,enComisionPara:''}}, () => { this.syncCabanaPrebidEvents();if (profile?.cuitCuil) this.checkCabanaPrebidCuit(profile.cuitCuil); });
24061    } catch(error) { this.showToast(error?.data?.detail || error.message || 'No se pudo consultar tu perfil.'); }
24062    finally { this._openingCabanaPrebid = false; }
24063  }
24064  closeCabanaPrebid() {
24065    if (this.state.cabanaPrebidSubmitting) return;
24066    this._cabanaPrebidCuitToken = (this._cabanaPrebidCuitToken || 0) + 1;
24067    this.setState({ cabanaPrebidModalOpen: false, cabanaPrebidCuitCheck:null, cabanaPrebidMessage: null },()=>this.syncCabanaPrebidEvents());
24068  }
24069  setCabanaPrebidField(key, value) {
24070    if (this.state.cabanaPrebidSubmitting) return;
24071    this.setState({ cabanaPrebidForm: { ...(this.state.cabanaPrebidForm || {}), [key]: value }, cabanaPrebidMessage: null });
24072    if (key === 'cuitCuil') this.checkCabanaPrebidCuit(value);
24073  }
24074  toggleCabanaPrebidCert(cert) {
24075    if (!this.cabanaPrebidAvailableCerts().includes(cert)) return;
24076    const form = this.state.cabanaPrebidForm || {};
24077    const selected = new Set(form.selectedIdentifiers || []);
24078    if (this.state.currentCabana?.prebid?.preofertaIndividual === true) { selected.clear(); selected.add(cert); }
24079    else if (selected.has(cert)) selected.delete(cert); else selected.add(cert);
24080    const selectedIdentifiers = [...selected];
24081    this.setState({cabanaPrebidForm:{...form,selectedIdentifiers,amount:this.cabanaPrebidMinimum(selectedIdentifiers)},cabanaPrebidMessage:null});
24082  }
24083  adjustCabanaPrebidAmount(direction) {
24084    const form = this.state.cabanaPrebidForm || {};
24085    const increment = this.cabanaPrebidIncrement();
24086    const winner = this.cabanaWinningPrebid();
24087    const minimum = this.cabanaPrebidMinimum();
24088    if (minimum == null) { this.showToast("Configurá la base y el incremento del lote."); return; }
24089    const current = Math.max(minimum, this.cabanaPrebidNumber(form.amount));
24090    const amount = Math.max(minimum, current + Number(direction || 0) * increment);
24091    if (this._cabanaPrebidRollTimer) clearTimeout(this._cabanaPrebidRollTimer);
24092    this.setState({
24093      cabanaPrebidForm: { ...form, amount },
24094      cabanaPrebidMessage: null,
24095      cabanaPrebidRolling: true,
24096    });
24097    this._cabanaPrebidRollTimer = setTimeout(() => {
24098      this._cabanaPrebidRollTimer = null;
24099      this.setState({ cabanaPrebidRolling: false });
24100    }, 760);
24101  }
24102  cabanaPrebidCuitReady() {
24103    const cuit = String(this.state.cabanaPrebidProfile?.cuitCuil || this.state.cabanaPrebidForm?.cuitCuil || '').replace(/\D/g, '');
24104    const check = this.state.cabanaPrebidCuitCheck;
24105    return !!(check?.status === 'valid' && check.cuit === cuit && check.uid === this.state.user?.uid);
24106  }
24107  async checkCabanaPrebidCuit(value) {
24108    const cuit = String(value || '').replace(/\D/g, '');
24109    const token = this._cabanaPrebidCuitToken = (this._cabanaPrebidCuitToken || 0) + 1;
24110    const uid = this.state.user?.uid;
24111    if (cuit.length !== 11) { this.setState({cabanaPrebidCuitCheck:null}); return; }
24112    this.setState({cabanaPrebidCuitCheck:{cuit,uid,status:'loading',text:'Validando CUIT/CUIL en ARCA…'}});
24113    try {
24114      const result = await this.validateCabanaPrebidCuit(cuit);
24115      if (token !== this._cabanaPrebidCuitToken || uid !== this.state.user?.uid || !this.state.cabanaPrebidModalOpen) return;
24116      this.setState({cabanaPrebidCuitCheck:{cuit,uid,status:'valid',text:'Validado en ARCA: ' + result.name}});
24117    } catch (error) {
24118      if (token !== this._cabanaPrebidCuitToken || uid !== this.state.user?.uid || !this.state.cabanaPrebidModalOpen) return;
24119      this.setState({cabanaPrebidCuitCheck:{cuit,uid,status:'error',text:error?.data?.detail || error?.message || 'No se pudo validar en ARCA. Reintentá.'}});
24120    }
24121  }
24122  async validateCabanaPrebidCuit(value) {
24123    const cuit = String(value || '').replace(/\D/g, '');
24124    if (cuit.length !== 11) throw new Error('Ingresá un CUIT/CUIL válido de 11 dígitos.');
24125    const response = await this.apiFetch(this.roslotCuitLookupEndpoint(cuit));
24126    const data = response?.data ?? response ?? {};
24127    if (data.error) throw new Error(String(data.error));
24128    const record = data.comitente || data.item || data;
24129    const source = String(data.source || record.source || '').toLowerCase();
24130    const name = String(record.nombreFiscal || record.fiscalName || record.razonSocial || record.name || record.nombre || '').trim();
24131    const returnedCuit = String(record.cuitCuil || record.cuitNormalized || record.cuit || '').replace(/\D/g, '');
24132    if (data.found === false || !name) throw new Error('ARCA no encontró este CUIT/CUIL. Revisá el número ingresado.');
24133    if (source === 'local') throw new Error('El CUIT/CUIL figura en los registros locales, pero no se pudo validar en ARCA. Reintentá la consulta.');
24134    if (returnedCuit && returnedCuit !== cuit) throw new Error('El CUIT/CUIL devuelto por ARCA no coincide con el consultado.');
24135    return { cuit, name };
24136  }
24137  async submitCabanaPrebid() {
24138    const windowState = window.CabanaPrebid.windowState(this.state.currentCabana);
24139    if (!windowState.active) { this.setState({cabanaPrebidMessage:{ok:false,text:windowState.upcoming ? 'Las preofertas todavía no comenzaron.' : 'Las preofertas están cerradas.'}}); return; }
24140    if (!this.cabanaPrebidCuitReady()) { this.setState({cabanaPrebidMessage:{ok:false,text:'Validá el CUIT/CUIL en ARCA antes de confirmar la preoferta.'}}); return; }
24141    if (this.state.cabanaPrebidSubmitting) return;
24142    const form = this.state.cabanaPrebidForm || {};
24143    const digits = String(this.state.cabanaPrebidProfile?.cuitCuil || form.cuitCuil || '').replace(/\D/g, '');
24144    if (digits.length !== 11) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'Ingresá un CUIT/CUIL válido de 11 dígitos.' } }); return; }
24145    if (!this.state.user || !this.state.cabanaPrebidProfile) {this.showToast('Volvé a abrir el formulario para verificar tu perfil.');return;}
24146    const selectedCerts = (form.selectedIdentifiers || []).map(String);
24147    if (!window.CabanaPrebid.validateSelection(selectedCerts,this.cabanaPrebidAvailableCert
24147s(),this.state.currentCabana?.prebid?.preofertaIndividual === true)) { this.setState({cabanaPrebidMessage:{ok:false,text:'Seleccioná los animales del lote sin duplicados; en modo individual, elegí uno solo.'}}); return; }
24148    if (!selectedCerts.length) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'Seleccioná al menos un animal.' } }); return; }
24149    if (form.enComision && !String(form.enComisionPara || '').trim()) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'Indicá para quién realizás la preoferta.' } }); return; }
24150    const amount = this.cabanaPrebidNumber(form.amount);
24151    const winner = this.cabanaWinningPrebid();
24152    const increment = this.cabanaPrebidIncrement();
24153    const minimum = this.cabanaPrebidMinimum();
24154    if (minimum == null) { this.showToast("Configurá la base y el incremento del lote."); return; }
24155    if (amount < minimum) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'El monto mínimo para esta preoferta es ' + this.fmtARS(minimum) + '.' } }); return; }
24156    if ((amount - minimum) % increment !== 0) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'El monto debe avanzar en incrementos de ' + this.fmtARS(increment) + '.' } }); return; }
24157    const auctionId = this.cabanaRemateId(this.state.currentCabana);
24158    const lotCert = this.cabanaPrebidCert(this.state.currentCabanaLot);
24159    if (!auctionId || !lotCert) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'No se pudo identificar el lote.' } }); return; }
24160    this.setState({ cabanaPrebidSubmitting: true, cabanaPrebidMessage: null });
24161    try {
24162      this.setState({ cabanaPrebidMessage: { ok: true, text: 'Validando CUIT/CUIL en ARCA…' } });
24163      if (!this.cabanaPrebidCuitReady()) throw new Error('Volvé a validar el CUIT/CUIL en ARCA.');
24164      if (String(this.state.cabanaPrebidProfile?.cuitCuil || this.state.cabanaPrebidForm?.cuitCuil || '').replace(/\D/g, '') !== digits) throw new Error('El CUIT/CUIL cambió durante la consulta. Volvé a confirmar la preoferta.');
24165      this.setState({ cabanaPrebidMessage: null });
24166      const result = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/prebids', {
24167        method: 'POST',
24168        body: JSON.stringify({ cuitCuil: this.state.cabanaPrebidProfile?.cuitCuil ?null : this.cuitInputDisplay(form.cuitCuil), amount, selectedCerts, comisionFor: null, enComisionPara: form.enComision ?String(form.enComisionPara || '').trim() : null }),
24169      });
24170      const resultRoot = result?.data ?? result;
24171      const status = String(resultRoot?.status || resultRoot?.estado || 'Registrada');
24172      const pending = /pendiente/i.test(status);
24173      const currentLot = this.state.currentCabanaLot;
24174      const currentAuction = this.state.currentCabana || {};
24175      if (this._cabanaPrebidSuccessTimer) clearTimeout(this._cabanaPrebidSuccessTimer);
24176      this.setState({
24177        cabanaPrebidSubmitting: false,
24178        cabanaPrebidModalOpen: false,
24179        cabanaPrebidMessage: null,
24180        cabanaPrebidRolling: false,
24181        cabanaPrebidSuccessFx: {
24182          eyebrow: pending ?'PREOFERTA RECIBIDA' : 'PREOFERTA REALIZADA',
24183          title: pending ?'¡Preoferta enviada!' : '¡Tu preoferta se realizó con éxito!',
24184          amount: this.fmtARS(amount),
24185          message: pending ?'Quedó pendiente de aprobación.' : 'La registramos correctamente para este lote.',
24186        },
24187      });
24188
24189      const lotNumber = this.cabanaLotNumber(currentLot);
24190      const auctionName = String(currentAuction?.nombre_remate || currentAuction?.title || currentAuction?.titulo || '').trim();
24191      const consignatariaValue = currentAuction?.consignataria;
24192      const consignatariaName = String(currentAuction?.consignatariaName || currentAuction?.consignatariaNombre || (typeof consignatariaValue === 'string' ?consignatariaValue : (consignatariaValue?.name || consignatariaValue?.nombre)) || currentAuction?.nombre_cuenta || '').trim();
24193      const endAt = currentAuction?.prebid?.fechaHoraFinPreoferta || currentAuction?.prebid?.fin || currentAuction?.prebid?.endAt || currentAuction?.prebid?.endDate || '';
24194      const ownOffer = {
24195        id: String(resultRoot?.id || resultRoot?.prebidId || ''),
24196        amount,
24197        status,
24198        selectedCerts,
24199        createdAt: new Date().toISOString(),
24200      };
24201      this.persistCabanaPrebidOwnOffer(auctionId, lotCert, ownOffer);
24202      this.setState((previous) => ({
24203        cabanaPrebidMine: [...(Array.isArray(previous.cabanaPrebidMine) ?previous.cabanaPrebidMine : []), ownOffer],
24204      }));
24205      await this.apiFetch('/api/notifications/cabana-prebid', {
24206        method: 'POST',
24207        body: JSON.stringify({
24208          auctionId,
24209          lotCert,
24210          eventId: resultRoot?.id || resultRoot?.prebidId || '',
24211          status,
24212          amount,
24213          selectedCerts,
24214          auctionName,
24215          lotLabel: lotNumber ?('lote ' + lotNumber) : ('lote ' + lotCert),
24216          consignatariaId: currentAuction?.consignatariaId || currentAuction?.consignataria?.id || '',
24217          consignatariaName,
24218          endAt,
24219          href: '#/cabanas/' + encodeURIComponent(auctionId) + '/lote/' + encodeURIComponent(lotCert),
24220        }),
24221      }).catch((error) => {
24222        console.warn('No se pudieron distribuir las notificaciones de la preoferta.', error);
24223        return null;
24224      });
24225      const currentLotCert = this.cabanaPrebidCert(currentLot);
24226      let refreshedRows = null;
24227      const refreshedRowsPromise = this.fetchCabanaPrebids(currentLot, { apply: false, loading: false }).then((rows) => {
24228        refreshedRows = rows;
24229        return rows;
24230      });
24231      const applyRefreshedRows = (rows) => {
24232        if (!Array.isArray(rows) || !rows.length) {
24233          this.fetchCabanaPrebids(currentLot);
24234          return;
24235        }
24236        if (this.cabanaPrebidCert(this.state.currentCabanaLot) !== String(currentLotCert)) return;
24237        this.setState({ cabanaPrebids: rows, cabanaPrebidsLoading: false, cabanaPrebidsError: '' });
24238      };
24239      this._cabanaPrebidSuccessTimer = setTimeout(() => {
24240        this._cabanaPrebidSuccessTimer = null;
24241        this.setState({ cabanaPrebidSuccessFx: null }, () => {
24242          if (refreshedRows) applyRefreshedRows(refreshedRows);
24243          else refreshedRowsPromise.then(applyRefreshedRows);
24244        });
24245      }, 1850);
24246    } catch (error) {
24247      if (error?.status === 400) {await this.fetchCabanaPrebids();const amount=this.cabanaPrebidMinimum();if(amount!=null)this.setState({cabanaPrebidForm:{...this.state.cabanaPrebidForm,amount}});}
24248      this.setState({ cabanaPrebidSubmitting: false, cabanaPrebidMessage: { ok: false, text: window.CabanaPrebid.errorMessage(error, 'No se pudo enviar la preoferta.') } });
24249    }
24250  }
24251  cabanaLotId(lot) {
24252    return String(lot?.lotCert || lot?.lotId || lot?.id || '').trim();
24253  }
24254  cabanaLotNumber(lot) {
24255    return window.CabanaLotIdentity.resolve(lot, this.state.currentCabana).number;
24256  }
24257  cabanaPublicLotPath(lot) {
24258    const lotNumber = this.cabanaLotNumber(lot) || this.cabanaLotId(lot);
24259    const rp = this.cabanaAnalyticsRpId(lot) || lotNumber;
24260    if (this.state.publicAdvanceMode === 'lacassina') {
24261      if (!lotNumber || !rp) return '/lacassina';
24262      return '/lacassina/' + encodeURIComponent(String(lotNumber).trim().toLowerCase()) + '/' + encodeURIComponent(String(rp).trim().toLowerCase());
24263    }
24264    const auctionId = this.cabanaRemateId(this.state.currentCabana) || String(lot?.remateId || lot?.auctionId || '').trim();
24265    const lotId = this.cabanaLotId(lot) || lotNumber;
24266    if (!auctionId) return '/cabanas';
24267    if (!lotId) return '/cabanas/' + encodeURIComponent(auctionId);
24268    return '/cabanas/' + encodeURIComponent(auctionId) + '/lotes/' + encodeURIComponent(lotId);
24269  }
24270  syncLacassinaLotPath(lot) {
24271    if (this.state.publicAdvanceMode !== 'lacassina' || window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-stats') return;
24272    const nextPath = this.cabanaPublicLotPath(lot);
24273    if (!nextPath || window.location.pathname === nextPath) return;
24274    this.writeAppHistory(nextPath);
24275    window.__ROSGAN_PUBLIC_PATH__ = nextPath;
24276    window.__ROSGAN_PUBLIC_LOT_NUMBER__ = this.cabanaLotNumber(lot) || '';
24277    window.__ROSGAN_PUBLIC_RP__ = this.cabanaAnalyticsRpId(lot) || '';
24278  }
24279  cabanaLotGroupKey(lot, index = 0) {
24280    const category = lot?.categoryId || lot?.categoryName || 'categoria';
24281    const number = this.cabanaLotNumber(lot);
24282    const fallback = lot?.groupId || lot?.lotCert || lot?.id || index;
24283    return this.consKey(category) + '::' + this.consKey(number || fallback);
24284  }
24285  cabanaLotVariantId(lot) {
24286    return String(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || lot?.id || '').trim();
24287  }
24288  startCabanaCarouselCrossfade(groupKey, updateState) {
24289    if (this._cabanaCarouselFadeTimer) clearTimeout(this._cabanaCarouselFadeTimer);
24290    if (this._cabanaCarouselRevealTimer) clearTimeout(this._cabanaCarouselRevealTimer);
24291    const activeCards = this._cabanaActiveCardByGroup || {};
24292    const previousMedia = groupKey === '*'
24293      ?Object.fromEntries(Object.entries(activeCards).map(([key, card]) => [key, {
24294        media: card.media,
24295        hasMedia: card.hasMedia,
24296        backgroundLogo: card.backgroundLogo,
24297        hasBackgroundLogo: card.hasBackgroundLogo,
24298        backgroundLogoFilter: card.backgroundLogoFilter,
24299      }]))
24300      :{
24301        ...(this.state.cbCarouselPreviousMedia || {}),
24302        [groupKey]: activeCards[groupKey] ?{
24303          media: activeCards[groupKey].media,
24304          hasMedia: activeCards[groupKey].hasMedia,
24305          backgroundLogo: activeCards[groupKey].backgroundLogo,
24306          hasBackgroundLogo: activeCards[groupKey].hasBackgroundLogo,
24307          backgroundLogoFilter: activeCards[groupKey].backgroundLogoFilter,
24308        } : null,
24309      };
24310    this.setState((previous) => ({
24311      ...(typeof updateState === 'function' ?updateState(previous) : {}),
24312      cbCarouselFading: true,
24313      cbCarouselFadingKey: groupKey,
24314      cbCarouselCrossfadeActive: false,
24315      cbCarouselPreviousMedia: previousMedia,
24316    }), () => {
24317      const nextCards = this._cabanaActiveCardByGroup || {};
24318      const affectedKeys = groupKey === '*' ?Object.keys(previousMedia) : [groupKey];
24319      const hasRenderableNextMedia = affectedKeys.some((key) => {
24320        const nextCard = nextCards[key];
24321        return !!(nextCard?.hasMedia || nextCard?.hasBackgroundLogo);
24322      });
24323      if (!hasRenderableNextMedia) this.revealCabanaCarouselCrossfade();
24324    });
24325  }
24326  revealCabanaCarouselCrossfade() {
24327    if (!this.state.cbCarouselFading || this.state.cbCarouselCrossfadeActive) return;
24328    if (this._cabanaCarouselRevealTimer) clearTimeout(this._cabanaCarouselRevealTimer);
24329    this.setState({ cbCarouselCrossfadeActive: true }, () => {
24330      this._cabanaCarouselRevealTimer = setTimeout(() => {
24331        this.setState({
24332          cbCarouselFading: false,
24333          cbCarouselFadingKey: '',
24334          cbCarouselCrossfadeActive: false,
24335          cbCarouselPreviousMedia: {},
24336        });
24337      }, 370);
24338    });
24339  }
24340  selectCabanaCarouselSlide(groupKey, index, event) {
24341    event?.preventDefault?.();
24342    event?.stopPropagation?.();
24343    this.startCabanaCarouselCrossfade(groupKey, (previous) => ({
24344      cbCarouselOffsets: {
24345        ...(previous.cbCarouselOffsets || {}),
24346        [groupKey]: Number(index) - Number(previous.cbCarouselStep || 0),
24347      },
24348    }));
24349  }
24350  selectCabanaLotVariant(lot, event) {
24351    event?.preventDefault?.();
24352    event?.stopPropagation?.();
24353    const lotId = this.cabanaLotId(lot);
24354    const selectedLot = { ...lot, videos: Array.isArray(lot?.videos) ?lot.videos : [], __videosLoaded: true };
24355    this.flushCabanaLotViewTimer('switch_rp');
24356    this.syncLacassinaLotPath(lot);
24357    this.trackCabanaAnalyticsOnce('rp_view', lot, { metadata: { action: 'select_rp' } });
24358    this.startCabanaLotViewTimer(lot);
24359    this.setState({
24360      currentCabanaLot: selectedLot,
24361      cbLotVideo: 0,
24362      cabanaLotVideosLoading: false,
24363      cabanaLotMediaReady: false,
24364      cabanaLotMediaFailed: false,
24365      cabanaLotVideoPlaying: false,
24366      cabanaLotVideoProgress: 0,
24367      cabanaLotVideoDuration: 0,
24368      cabanaLotVideoVolume: 0,
24369      cabanaLotVideoMuted: true,
24370    }, () => {
24371      this.syncCabanaLotVideo(lotId);
24372      this.fetchCabanaPrebids(lot);
24373    });
24374  }
24375  syncCabanaLotVideo(expectedLotId) {
24376    window.setTimeout(() => {
24377      if (!expectedLotId || this.cabanaLotId(this.state.currentCabanaLot) !== String(expectedLotId)) return;
24378      const player = document.querySelector('video.dc-cabana-lot-player');
24379      const videos = this.cabanaLotVideos(this.state.currentCabanaLot);
24380      const activeVideo = videos[Math.max(0, Number(this.state.cbLotVideo || 0))];
24381      if (!player || !activeVideo?.url) return;
24382      const expectedUrl = new URL(activeVideo.url, window.location.href).href;
24383      if (player.currentSrc !== expectedUrl && player.src !== expectedUrl) player.src = activeVideo.url;
24384      player.autoplay = true;
24385      player.muted = true;
24386      player.loop = true;
24387      player.load();
24388      player.play?.().catch(() => {});
24389      this.syncCabanaVideoState(player, true);
24390    }, 0);
24391  }
24392  cabanaLotPlayer() {
24393    return document.querySelector('video.dc-cabana-lot-player');
24394  }
24395  formatCabanaVideoTime(seconds) {
24396    const value = Math.max(0, Math.floor(Number(seconds) || 0));
24397    const minutes = Math.floor(value / 60);
24398    const rest = value % 60;
24399    return minutes + ':' + String(rest).padStart(2, '0');
24400  }
24401  syncCabanaVideoState(video = this.cabanaLotPlayer(), force = false) {
24402    if (!video) return;
24403    const now = Date.now();
24404    if (!force && this._cabanaVideoStateAt && now - this._cabanaVideoStateAt < 350) return;
24405    this._cabanaVideoStateAt = now;
24406    const duration = Number.isFinite(video.duration) ?video.duration : 0;
24407    const progress = duration > 0 ?Math.min(100, Math.max(0, (Number(video.currentTime || 0) / duration) * 100)) : 0;
24408    this.setState({
24409      cabanaLotVideoPlaying: !video.paused && !video.ended,
24410      cabanaLotVideoProgress: progress,
24411      cabanaLotVideoDuration: duration,
24412      cabanaLotVideoVolume: video.muted ?0 : Number(video.volume || 0),
24413      cabanaLotVideoMuted: !!video.muted || Number(video.volume || 0) === 0,
24414    });
24415  }
24416  toggleCabanaVideoPlayback(event) {
24417    event?.preventDefault?.();
24418    const video = this.cabanaLotPlayer();
24419    if (!video) return;
24420    if (video.paused) video.play?.().catch(() => {});
24421    else video.pause?.();
24422    this.syncCabanaVideoState(video, true);
24423  }
24424  seekCabanaVideo(event) {
24425    const video = this.cabanaLotPlayer();
24426    if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return;
24427    const value = Math.max(0, Math.min(100, Number(event?.target?.value || 0
24427)));
24428    video.currentTime = (value / 100) * video.duration;
24429    this.syncCabanaVideoState(video, true);
24430  }
24431  setCabanaVideoVolume(event) {
24432    const video = this.cabanaLotPlayer();
24433    if (!video) return;
24434    const value = Math.max(0, Math.min(1, Number(event?.target?.value || 0)));
24435    try { video.volume = value; } catch (_) {}
24436    video.muted = value <= 0;
24437    this.syncCabanaVideoState(video, true);
24438  }
24439  toggleCabanaVideoMute(event) {
24440    event?.preventDefault?.();
24441    const video = this.cabanaLotPlayer();
24442    if (!video) return;
24443    const nextMuted = !video.muted && Number(video.volume || 0) > 0;
24444    if (!nextMuted && Number(video.volume || 0) <= 0) {
24445      try { video.volume = 0.8; } catch (_) {}
24446    }
24447    video.muted = nextMuted;
24448    this.syncCabanaVideoState(video, true);
24449  }
24450  async openCabanaVideoFullscreen(event) {
24451    event?.preventDefault?.();
24452    const video = this.cabanaLotPlayer();
24453    if (!video) return;
24454    const frame = video.closest('.dc-cabana-video-frame') || video;
24455    try {
24456      if (frame.requestFullscreen) await frame.requestFullscreen();
24457      else if (video.requestFullscreen) await video.requestFullscreen();
24458      else if (video.webkitEnterFullscreen) {
24459        video.webkitEnterFullscreen();
24460        return;
24461      }
24462      if (screen?.orientation?.lock) screen.orientation.lock('landscape').catch(() => {});
24463      if (!this._cabanaFullscreenListener) {
24464        this._cabanaFullscreenListener = () => {
24465          if (!document.fullscreenElement && screen?.orientation?.unlock) {
24466            try { screen.orientation.unlock(); } catch (_) {}
24467          }
24468        };
24469        document.addEventListener('fullscreenchange', this._cabanaFullscreenListener);
24470      }
24471    } catch (_) {
24472      try { video.webkitEnterFullscreen?.(); } catch (__) {}
24473    }
24474  }
24475  async fetchCabanaLotVideos(lot) {
24476    const auctionId = String(lot?.remateId || this.cabanaRemateId(this.state.currentCabana) || '').trim();
24477    const lotId = this.cabanaLotId(lot);
24478    if (!auctionId || !lotId) {
24479      if (this.cabanaLotId(this.state.currentCabanaLot) === lotId) {
24480        this.setState({
24481          currentCabanaLot: { ...lot, __videosLoaded: true },
24482          cabanaLotVideosLoading: false,
24483        });
24484      }
24485      return;
24486    }
24487    if (lot?.__videosLoaded !== true) this.setState({ cabanaLotVideosLoading: true });
24488    try {
24489      const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotId) + '/videos');
24490      let videos = this.listFrom(data, ['items', 'data', 'videos', 'media', 'results', 'value']);
24491      const singleVideo = data?.video || data?.data?.video || data?.data || data?.item || data;
24492      if (!videos.length && singleVideo && typeof singleVideo === 'object' && (singleVideo.url || singleVideo.videoUrl || singleVideo.downloadUrl || singleVideo.streamUrl || singleVideo.mediaUrl || singleVideo.src)) videos = [singleVideo];
24493      const normalizedVideos = videos.map((video) => typeof video === 'string' ?{ url: video } : video).filter(Boolean);
24494      const sameLot = (item) => this.cabanaLotId(item) === lotId;
24495      const mergedLot = { ...lot, videos: normalizedVideos, __videosLoaded: true };
24496      this._cabanaTvVideoCache = this._cabanaTvVideoCache || new Map();
24497      this._cabanaTvVideoCache.set(auctionId + ':' + lotId, { videos: normalizedVideos, loaded: true });
24498      const isCurrentLot = sameLot(this.state.currentCabanaLot);
24499      this.setState((previous) => {
24500        const previousCurrentLot = previous.currentCabanaLot;
24501        const currentStillMatches = sameLot(previousCurrentLot);
24502        return {
24503          ...(currentStillMatches ?{
24504            currentCabanaLot: { ...previousCurrentLot, videos: normalizedVideos, __videosLoaded: true },
24505            cbLotVideo: 0,
24506            cabanaLotVideosLoading: false,
24507            cabanaLotMediaReady: false,
24508            cabanaLotMediaFailed: false,
24509          } : {}),
24510          cabanaLots: Array.isArray(previous.cabanaLots) ?previous.cabanaLots.map((item) => sameLot(item) ?{ ...item, videos: normalizedVideos, __videosLoaded: true } : item) : previous.cabanaLots,
24511        };
24512      });
24513      if (isCurrentLot) this.syncCabanaLotVideo(lotId);
24514      return mergedLot;
24515    } catch {
24516      this._cabanaTvVideoCache = this._cabanaTvVideoCache || new Map();
24517      this._cabanaTvVideoCache.set(auctionId + ':' + lotId, { videos: [], loaded: true, failed: true });
24518      const sameLot = (item) => this.cabanaLotId(item) === lotId;
24519      this.setState((previous) => ({
24520        ...(sameLot(previous.currentCabanaLot) ?{
24521          currentCabanaLot: { ...previous.currentCabanaLot, __videosLoaded: true },
24522          cabanaLotVideosLoading: false,
24523        } : {}),
24524        cabanaLots: Array.isArray(previous.cabanaLots)
24525          ?previous.cabanaLots.map((item) => sameLot(item) ?{ ...item, __videosLoaded: true } : item)
24526          : previous.cabanaLots,
24527      }));
24528    }
24529  }
24530  cabanaLotVideos(lot) {
24531    const raw = Array.isArray(lot?.videos) ?lot.videos : [];
24532    return raw.map((video) => typeof video === 'string'
24533      ?{ url: video, thumb: '' }
24534      : {
24535        id: video?.id || video?.videoId || video?.mediaId || '',
24536        url: video?.url || video?.videoUrl || video?.downloadUrl || video?.streamUrl || video?.mediaUrl || video?.src || '',
24537        thumb: video?.thumbnailUrl || video?.thumb || video?.posterUrl || video?.coverUrl || video?.coverImage?.url || '',
24538        fileName: video?.fileName || video?.name || '',
24539        associatedLotIds: Array.isArray(video?.associatedLotIds) ?video.associatedLotIds.map(String):null,
24540        videoType: video?.videoType || video?.type || '',
24541      }).filter((video) => video.url);
24542  }
24543  cabanaTvVideoItems(group) {
24544    const variants = Array.isArray(group?.variants) ?group.variants : [];
24545    const fallbackPoster = (lot) => this.cabanaLotSelectedCoverUrl(lot) || '';
24546    return variants.map((variant) => {
24547      const lotId = this.cabanaLotId(variant);
24548      const auctionId = String(variant?.remateId || this.cabanaRemateId(this.state.currentCabana) || '').trim();
24549      const cached = this._cabanaTvVideoCache?.get(auctionId + ':' + lotId);
24550      const videos = cached?.loaded === true ?this.cabanaLotVideos({ videos: cached.videos }) : this.cabanaLotVideos(variant);
24551      const firstVideo = videos[0] || {};
24552      const rp = this.cabanaAnalyticsRpId(variant) || this.cabanaLotVariantId(variant) || this.cabanaLotNumber(variant) || '';
24553      return {
24554        key: this.cabanaCommercialRowKey(variant) || this.cabanaLotId(variant) || rp,
24555        lotId,
24556        variant,
24557        rp: rp ?('RP ' + rp) : 'RP',
24558        src: firstVideo.url || '',
24559        poster: firstVideo.thumb || fallbackPoster(variant),
24560        needsLoad: !firstVideo.url && cached?.loaded !== true && variant?.__videosLoaded !== true,
24561      };
24562    }).filter((item) => item.src || item.needsLoad);
24563  }
24564  ensureCabanaTvVideos(group) {
24565    const variants = Array.isArray(group?.variants) ?group.variants : [];
24566    if (!variants.length) return;
24567    this._cabanaTvVideoFetches = this._cabanaTvVideoFetches || new Set();
24568    variants.forEach((variant) => {
24569      const lotId = this.cabanaLotId(variant);
24570      if (!lotId || variant?.__videosLoaded === true || this.cabanaLotVideos(variant).length) return;
24571      const key = (this.cabanaRemateId(this.state.currentCabana) || '2') + ':' + lotId;
24572      if (this._cabanaTvVideoFetches.has(key)) return;
24573      this._cabanaTvVideoFetches.add(key);
24574      this.fetchCabanaLotVideos(variant).finally(() => {
24575        this._cabanaTvVideoFetches.delete(key);
24576      });
24577    });
24578  }
24579  clearCabanaTvVideoRecovery(video) {
24580    if (!video || !this._cabanaTvVideoRecoveryTimers) return;
24581    const timer = this._cabanaTvVideoRecoveryTimers.get(video);
24582    if (timer) window.clearTimeout(timer);
24583    this._cabanaTvVideoRecoveryTimers.delete(video);
24584  }
24585  scheduleCabanaTvVideoRecovery(video, immediate = false) {
24586    if (!video?.isConnected) return;
24587    this._cabanaTvVideoRecoveryTimers = this._cabanaTvVideoRecoveryTimers || new Map();
24588    if (this._cabanaTvVideoRecoveryTimers.has(video) && !immediate) return;
24589    this.clearCabanaTvVideoRecovery(video);
24590    const timer = window.setTimeout(() => {
24591      this._cabanaTvVideoRecoveryTimers?.delete(video);
24592      if (!video.isConnected) return;
24593      const shouldPlay = video.dataset.tvShouldPlay === 'true';
24594      if (video.readyState >= 3 && (!shouldPlay || (!video.paused && !video.ended))) return;
24595      const attempts = Number(video.dataset.tvRecoveryAttempts || 0) + 1;
24596      video.dataset.tvRecoveryAttempts = String(attempts);
24597      const source = String(video.dataset.tvSource || video.currentSrc || video.src || '').trim();
24598      if (!source) return;
24599      if (attempts > 3) {
24600        const tile = video.closest?.('.tv-video-tile');
24601        const mount = tile?.parentElement;
24602        if (shouldPlay && tile?.classList.contains('is-active') && mount?.classList.contains('is-single') && mount.querySelectorAll('.tv-video-tile').length > 1) {
24603          video.dataset.tvRecoveryAttempts = '0';
24604          this.advanceCabanaTvPlaylist({ target: video });
24605          return;
24606        }
24607        video.dataset.tvRecoveryAttempts = '0';
24608      }
24609      const resumeAt = Number.isFinite(Number(video.currentTime)) ?Number(video.currentTime) : 0;
24610      try {
24611        video.classList.remove('is-ready');
24612        video.src = source;
24613        video.load();
24614        if (resumeAt > 0) video.addEventListener('loadedmetadata', () => {
24615          try { video.currentTime = Math.min(resumeAt, Math.max(0, Number(video.duration) - .2)); } catch (error) {}
24616        }, { once: true });
24617        if (shouldPlay) {
24618          const playResult = video.play?.();
24619          if (playResult?.catch) playResult.catch(() => this.scheduleCabanaTvVideoRecovery(video));
24620        }
24621      } catch (error) {
24622        this.scheduleCabanaTvVideoRecovery(video);
24623      }
24624    }, immediate ?350 : 4500);
24625    this._cabanaTvVideoRecoveryTimers.set(video, timer);
24626  }
24627  prepareCabanaTvVideo(video, shouldPlay = false) {
24628    if (!video) return;
24629    video.dataset.tvShouldPlay = shouldPlay ?'true' : 'false';
24630    if (!shouldPlay) {
24631      try { video.pause(); } catch (error) {}
24632      if (video.readyState < 3) {
24633        try { video.load?.(); } catch (error) {}
24634        this.scheduleCabanaTvVideoRecovery(video);
24635      }
24636      return;
24637    }
24638    const playResult = video.play?.();
24639    if (playResult?.catch) playResult.catch(() => this.scheduleCabanaTvVideoRecovery(video));
24640    if (video.readyState < 3 || video.paused) this.scheduleCabanaTvVideoRecovery(video);
24641  }
24642  syncCabanaTvVideoPlayback(playback = {}) {
24643    if (typeof document === 'undefined') return;
24644    const portal = document.getElementById('tv-video-portal');
24645    const mount = portal?.querySelector('#tv-video-mount');
24646    if (!portal || !mount?.classList.contains('is-single')) return;
24647    const video = mount.querySelector('.tv-video-tile.is-active video') || mount.querySelector('video');
24648    if (!video) return;
24649    const restartToken = String(Number(playback.restartToken || 0));
24650    if (portal.dataset.restartToken !== restartToken) {
24651      portal.dataset.restartToken = restartToken;
24652      try { video.currentTime = 0; } catch (error) {}
24653    }
24654    if (playback.playing === false) {
24655      this.prepareCabanaTvVideo(video, false);
24656      return;
24657    }
24658    this.prepareCabanaTvVideo(video, true);
24659  }
24660  syncCabanaTvSoldVisuals(items = [], mode = 'single') {
24661    if (typeof document === 'undefined') return;
24662    const mount = document.querySelector('#tv-video-mount');
24663    if (!mount) return;
24664    const safeItems = Array.isArray(items) ?items : [];
24665    Array.from(mount.querySelectorAll('.tv-video-tile')).forEach((tile, itemIndex) => {
24666      const isSold = mode === 'grid' && safeItems[itemIndex]?.isSold === true;
24667      tile.dataset.saleState = isSold ?'sold' : 'active';
24668      let soldBadge = tile.querySelector('.tv-video-sale-badge');
24669      if (isSold) {
24670        if (!soldBadge) {
24671          soldBadge = document.createElement('span');
24672          soldBadge.className = 'tv-video-sale-badge';
24673          const soldCheck = document.createElement('i');
24674          soldCheck.setAttribute('aria-hidden', 'true');
24675          soldCheck.textContent = '\u2713';
24676          const soldLabel = document.createElement('strong');
24677          soldLabel.textContent = 'Vendido';
24678          soldBadge.appendChild(soldCheck);
24679          soldBadge.appendChild(soldLabel);
24680          tile.appendChild(soldBadge);
24681        }
24682        const currentBadge = soldBadge;
24683        window.requestAnimationFrame(() => {
24684          if (tile.dataset.saleState !== 'sold') return;
24685          tile.classList.add('is-sold');
24686          currentBadge.classList.add('is-visible');
24687        });
24688        return;
24689      }
24690      tile.classList.remove('is-sold');
24691      if (!soldBadge) return;
24692      soldBadge.classList.remove('is-visible');
24693      const staleBadge = soldBadge;
24694      window.setTimeout(() => {
24695        if (tile.dataset.saleState === 'active' && staleBadge.parentNode === tile) staleBadge.remove();
24696      }, 520);
24697    });
24698  }
24699  syncCabanaTvVideoMount(items = [], mode = 'single', playback = {}) {
24700    if (typeof document === 'undefined') return;
24701    if (this.state.route !== 'cabanaTvLower') {
24702      this.destroyCabanaTvVideoMount();
24703      return;
24704    }
24705    const safeMode = mode === 'grid' ?'grid' : 'single';
24706    const safeItems = Array.isArray(items) ?items.filter((item) => item?.src) : [];
24707    const signature = safeMode + '|' + safeItems.map((item) => [item.key, item.src].join('~')).join('|');
24708    let portal = document.getElementById('tv-video-portal');
24709    const mountExists = !!portal?.querySelector('#tv-video-mount');
24710    if (this._cabanaTvVideoMountSignature === signature && mountExists) {
24711      const existingMount = portal.querySelector('#tv-video-mount');
24712      const prewarming = playback.prewarming === true;
24713      portal.classList.toggle('is-prewarming', prewarming);
24714      existingMount?.classList.toggle('is-without-lower-third', playback.lowerThirdVisible === false);
24715      existingMount?.classList.toggle('is-waiting-background', playback.waitingBackgroun
24715d === true);
24716      this.syncCabanaTvSoldVisuals(safeItems, safeMode);
24717      if (safeMode === 'single') this.syncCabanaTvVideoPlayback({ ...playback, playing: !prewarming && playback.playing !== false });
24718      else existingMount?.querySelectorAll('video').forEach((video) => this.prepareCabanaTvVideo(video, !prewarming));
24719      return;
24720    }
24721    this._cabanaTvVideoMountSignature = signature;
24722    if (!portal) {
24723      portal = document.createElement('div');
24724      portal.id = 'tv-video-portal';
24725      portal.className = 'tv-video-portal';
24726      const bg = document.createElement('div');
24727      bg.className = 'tv-video-bg';
24728      const mountNode = document.createElement('div');
24729      mountNode.id = 'tv-video-mount';
24730      portal.appendChild(bg);
24731      portal.appendChild(mountNode);
24732      document.body.appendChild(portal);
24733    } else if (portal.parentNode !== document.body) {
24734      document.body.appendChild(portal);
24735    }
24736    const mount = portal.querySelector('#tv-video-mount');
24737    if (!mount) return;
24738    portal.classList.toggle('is-prewarming', playback.prewarming === true);
24739    mount.className = 'tv-video-layer ' + (safeMode === 'grid' ?('is-grid is-count-' + Math.min(4, safeItems.length || 1)) :'is-single') + (playback.lowerThirdVisible === false ?' is-without-lower-third' : '') + (playback.waitingBackgroun
24739d === true ?' is-waiting-background' : '');
24740    mount.querySelectorAll('video').forEach((video) => this.clearCabanaTvVideoRecovery(video));
24741    while (mount.firstChild) mount.removeChild(mount.firstChild);
24742    safeItems.forEach((item, itemIndex) => {
24743      const tile = document.createElement('article');
24744      tile.className = 'tv-video-tile' + (safeMode === 'single' && itemIndex === 0 ?' is-active' : '');
24745      if (item.poster) tile.style.backgroundImage = 'url("' + String(item.poster).replace(/"/g, '\\"') + '")';
24746      tile.style.backgroundSize = 'cover';
24747      tile.style.backgroundPosition = 'center';
24748      const video = document.createElement('video');
24749      video.src = item.src;
24750      if (item.poster) video.poster = item.poster;
24751      video.autoplay = safeMode === 'grid' || (itemIndex === 0 && playback.playing !== false);
24752      video.muted = true;
24753      video.defaultMuted = true;
24754      video.playsInline = true;
24755      video.preload = 'auto';
24756      video.setAttribute('muted', '');
24757      video.setAttribute('playsinline', '');
24758      video.setAttribute('webkit-playsinline', '');
24759      if (video.autoplay) video.setAttribute('autoplay', '');
24760      video.loop = safeMode === 'grid' || (safeMode === 'single' && safeItems.length === 1);
24761      video.dataset.tvSource = item.src;
24762      video.dataset.tvRecoveryAttempts = '0';
24763      const markReady = () => {
24764        video.classList.add('is-ready');
24765        video.dataset.tvRecoveryAttempts = '0';
24766        this.clearCabanaTvVideoRecovery(video);
24767      };
24768      video.addEventListener('canplay', markReady);
24769      video.addEventListener('playing', markReady);
24770      video.addEventListener('loadeddata', markReady);
24771      video.addEventListener('waiting', () => this.scheduleCabanaTvVideoRecovery(video));
24772      video.addEventListener('stalled', () => this.scheduleCabanaTvVideoRecovery(video));
24773      video.addEventListener('error', () => this.scheduleCabanaTvVideoRecovery(video, true));
24774      if (safeMode === 'single' && safeItems.length > 1) {
24775        video.addEventListener('timeupdate', (event) => {
24776          const current = event.currentTarget;
24777          const currentTile = current?.closest?.('.tv-video-tile');
24778          const remaining = Number(current?.duration) - Number(current?.currentTime);
24779          const transitionLead = currentTile?.parentElement?.classList.contains('is-waiting-background') ?1.25 : .55;
24780          if (!currentTile?.classList.contains('is-active') || !Number.isFinite(remaining) || remaining > transitionLead || remaining <= 0) return;
24781          this.advanceCabanaTvPlaylist(event);
24782        });
24783        video.addEventListener('ended', (event) => this.advanceCabanaTvPlaylist(event));
24784      }
24785      const badge = document.createElement('span');
24786      badge.className = 'tv-video-badge';
24787      badge.textContent = item.rp || 'RP';
24788      tile.appendChild(video);
24789      tile.appendChild(badge);
24790      mount.appendChild(tile);
24791      const shouldPlay = playback.prewarming !== true && (safeMode === 'grid' || (itemIndex === 0 && playback.playing !== false));
24792      this.prepareCabanaTvVideo(video, shouldPlay);
24793    });
24794    this.syncCabanaTvSoldVisuals(safeItems, safeMode);
24795    if (safeMode === 'single') this.syncCabanaTvVideoPlayback({ ...playback, playing: playback.prewarming !== true && playback.playing !== false });
24796  }
24797  destroyCabanaTvVideoMount() {
24798    this._cabanaTvVideoMountSignature = '';
24799    if (typeof document === 'undefined') return;
24800    const portal = document.getElementById('tv-video-portal');
24801    portal?.querySelectorAll('video').forEach((video) => this.clearCabanaTvVideoRecovery(video));
24802    if (portal?.parentNode) portal.parentNode.removeChild(portal);
24803  }
24804  cabanaMediaUrl(media) {
24805    if (!media) return '';
24806    if (typeof media === 'string') return window.resolveCabanaMediaUrl(media);
24807    if (typeof media !== 'object') return '';
24808    return window.resolveCabanaMediaUrl(media.url || media.publicUrl || media.downloadUrl || media.imageUrl || media.src);
24809  }
24810  cabanaLotItemCoverUrl(lot) {
24811    return this.cabanaMediaUrl(lot?.coverImage || lot?.cover_image);
24812  }
24813  cabanaLotFallbackCoverUrl(lot) {
24814    return this.cabanaMediaUrl(lot?.lotCoverImage || lot?.lot_cover_image)
24815      || window.resolveCabanaMediaUrl(lot?.lotThumbnailUrl);
24816  }
24817  cabanaLotSelectedCoverUrl(lot) {
24818    return this.cabanaLotItemCoverUrl(lot) || this.cabanaLotFallbackCoverUrl(lot);
24819  }
24820  cabanaLotImages(lot) {
24821    const candidates = [
24822      lot?.coverImage,
24823      lot?.cover_image,
24824      lot?.thumbnailUrl,
24825      lot?.coverImageUrl,
24826      ...(Array.isArray(lot?.images) ?lot.images : []),
24827      ...(Array.isArray(lot?.imagenes) ?lot.imagenes : []),
24828      ...(Array.isArray(lot?.photos) ?lot.photos : []),
24829      ...(Array.isArray(lot?.fotos) ?lot.fotos : []),
24830      ...(Array.isArray(lot?.media) ?lot.media : []),
24831    ];
24832    const seen = new Set();
24833    return candidates.map((media) => {
24834      if (!media) return null;
24835      if (typeof media === 'string') return { url: window.resolveCabanaMediaUrl(media), fileName: '' };
24836      if (typeof media !== 'object') return null;
24837      return {
24838        url: window.resolveCabanaMediaUrl(media.url || media.publicUrl || media.downloadUrl || media.imageUrl || media.src),
24839        fileName: media.fileName || media.name || media.nombre || '',
24840      };
24841    }).filter((media) => {
24842      if (!media?.url || seen.has(media.url)) return false;
24843      seen.add(media.url);
24844      return true;
24845    });
24846  }
24847  cabanaAnalyticsRpId(lot) {
24848    return String(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || '').trim();
24849  }
24850  cabanaAnalyticsOnceKey(eventType, lot = null, extra = {}) {
24851    return [
24852      String(lot?.remateId || this.cabanaRemateId(this.state.currentCabana) || ''),
24853      eventType,
24854      this.cabanaLotId(lot),
24855      this.cabanaAnalyticsRpId(lot),
24856      extra.mediaId || extra.videoId || extra.imageId || '',
24857    ].join('|');
24858  }
24859  trackCabanaAnalyticsOnce(eventType, lot = null, extra = {}) {
24860    if (!this._cabanaAnalyticsSeen) this._cabanaAnalyticsSeen = new Set();
24861    const key = this.cabanaAnalyticsOnceKey(eventType, lot, extra);
24862    if (this._cabanaAnalyticsSeen.has(key)) return;
24863    this._cabanaAnalyticsSeen.add(key);
24864    this.trackCabanaAnalytics(eventType, lot, extra);
24865  }
24866  startCabanaLotViewTimer(lot) {
24867    this.flushCabanaLotViewTimer('switch_lot');
24868    if (this._cabanaLotViewHeartbeat) {
24869      clearInterval(this._cabanaLotViewHeartbeat);
24870      this._cabanaLotViewHeartbeat = null;
24871    }
24872    if (!lot || !String(lot?.remateId || this.cabanaRemateId(this.state.currentCabana) || '').trim()) return;
24873    const lotId = this.cabanaLotId(lot);
24874    const rpId = this.cabanaAnalyticsRpId(lot);
24875    this._cabanaLotViewTimer = {
24876      lot,
24877      startedAt: Date.now(),
24878      rpId,
24879      viewId: [this.cabanaAnalyticsClientId(), lotId, rpId || 'rp', Date.now()].join('|'),
24880    };
24881    this._cabanaLotViewHeartbeat = setInterval(() => {
24882      this.flushCabanaLotViewTimer('heartbeat', true);
24883    }, 10000);
24884  }
24885  flushCabanaLotViewTimer(reason = 'leave_lot', keepActive = false) {
24886    const active = this._cabanaLotViewTimer;
24887    if (!active?.lot) {
24888      this._cabanaLotViewTimer = null;
24889      if (this._cabanaLotViewHeartbeat) {
24890        clearInterval(this._cabanaLotViewHeartbeat);
24891        this._cabanaLotViewHeartbeat = null;
24892      }
24893      return;
24894    }
24895    const durationMs = Date.now() - Number(active.startedAt || Date.now());
24896    if (keepActive) active.startedAt = Date.now();
24897    if (!keepActive) {
24898      this._cabanaLotViewTimer = null;
24899      if (this._cabanaLotViewHeartbeat) {
24900        clearInterval(this._cabanaLotViewHeartbeat);
24901        this._cabanaLotViewHeartbeat = null;
24902      }
24903    }
24904    if (durationMs < 3000) return;
24905    this.trackCabanaAnalytics('view_time', active.lot, {
24906      durationMs,
24907      metadata: { action: 'view_time', reason, viewId: active.viewId, cumulative: false, interval: true },
24908    });
24909  }
24910  trackCabanaVideoPlay(lot, videoUrl, mediaIndex = 0) {
24911    if (!videoUrl) return;
24912    this.trackCabanaAnalyticsOnce('video_play', lot, {
24913      mediaId: videoUrl,
24914      videoId: videoUrl,
24915      mediaType: 'video',
24916      metadata: { action: 'video_play', mediaIndex },
24917    });
24918  }
24919  cabanaAnalyticsClientId() {
24920    const key = 'rosgan:lacassinaAnalyticsClientId';
24921    try {
24922      const existing = localStorage.getItem(key);
24923      if (existing) return existing;
24924      const next = (window.crypto?.randomUUID?.() || ('client-' + Date.now() + '-' + Math.random().toString(36).slice(2)));
24925      localStorage.setItem(key, next);
24926      return next;
24927    } catch {
24928      return 'client-' + Date.now();
24929    }
24930  }
24931  cabanaSocialKey(lot = null) {
24932    const current = this.state.currentCabana || {};
24933    return [
24934      this.cabanaRemateId(current) || '2',
24935      this.cabanaLotId(lot) || 'lot',
24936      this.cabanaAnalyticsRpId(lot) || 'rp',
24937    ].join('|');
24938  }
24939  bumpCabanaSocialMetric(metric, lot = null) {
24940    const key = this.cabanaSocialKey(lot);
24941    const bumps = this.state.cabanaSocialBumps || {};
24942    const current = bumps[key] || {};
24943    this.setState({
24944      cabanaSocialBumps: {
24945        ...bumps,
24946        [key]: { ...current, [metric]: Number(current[metric] || 0) + 1 },
24947      },
24948    });
24949  }
24950  cabanaPublicShareUrl(lot = null) {
24951    const path = lot ?this.cabanaPublicLotPath(lot) : (this.state.currentCabana ?this.cabanaPath(this.state.currentCabana) : '/cabanas');
24952    try { return new URL(path, window.location.origin).href; }
24953    catch { return path; }
24954  }
24955  async shareCabanaLot(lot = null) {
24956    const target = lot || this.state.currentCabanaLot;
24957    if (!target) return;
24958    const { title, auctionTitle } = this.cabanaLotShareTitle(target);
24959    const url = this.cabanaPublicShareUrl(target);
24960    let shared = false;
24961    try {
24962      if (navigator.share) {
24963        await navigator.share({ title, text: title + ' - ' + auctionTitle, url });
24964        shared = true;
24965      } else if (navigator.clipboard?.writeText) {
24966        await navigator.clipboard.writeText(url);
24967        this.showToast('Enlace copiado');
24968        shared = true;
24969      } else {
24970        window.prompt('Copiar enlace', url);
24971        shared = true;
24972      }
24973    } catch (error) {
24974      if (error?.name !== 'AbortError') this.showToast('No se pudo compartir.');
24975      return;
24976    }
24977    if (!shared) return;
24978    const socialKey = this.cabanaSocialKey(target);
24979    this.bumpCabanaSocialMetric('shares', target);
24980    this.trackCabanaAnalytics('share_click', target, {
24981      metadata: { action: 'share_rp', url },
24982    });
24983    setTimeout(() => {
24984      this.fetchCabanaAnalyticsSummary(this.state.currentCabana).finally(() => {
24985        const bumps = { ...(this.state.cabanaSocialBumps || {}) };
24986        if (!bumps[socialKey]) return;
24987        delete bumps[socialKey];
24988        this.setState({ cabanaSocialBumps: bumps });
24989      });
24990    }, 1500);
24991  }
24992  cabanaLotValue(lot, keys, fallback = '') {
24993    for (const key of keys) {
24994      const value = lot?.[key] ?? lot?.values?.[key] ?? lot?.subformValues?.datos_principales?.[key];
24995      if (value !== undefined && value !== null && value !== '') return value;
24996    }
24997    return fallback;
24998  }
24999  cabanaStoryValueIsEmpty(value) {
25000    const text = String(value || '').trim();
25001    return !text || text === '-' || /^[-\u2013\u2014]+$/.test(text) || text.toLowerCase() === 'null' || text.toLowerCase() === 'undefined';
25002  }
25003  cabanaStoryCleanLabel(value) {
25004    return String(value || '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
25005  }
25006  cabanaStoryDisplayValue(value) {
25007    if (Array.isArray(value)) return value.map((item) => this.cabanaStoryDisplayValue(item)).filter((item) => !this.cabanaStoryValueIsEmpty(item)).join(', ');
25008    if (value && typeof value === 'object') return value.name || value.nombre || value.label || value.value || '';
25009    if (typeof value === 'boolean') return value ?'Si' : 'No';
25010    return value === undefined || value === null ?'' : String(value);
25011  }
25012  cabanaInstallmentField(lot,field) {
25013    const config=this.state.currentCabana?.installmentConfig;
25014    if(!config?.configured||![field?.key,field?.name].some(v=>this.consKey(v)==='preoferta'))return null;
25015    const payment=window.CabanaInstallments.resolve(config,lot);
25016    return payment.enabled?'Se preoferta la cuota ('+payment.count+' cuotas)':'Se preoferta el importe total';
25017  }
25018  cabanaLotFieldValue(lot, field, form = {}) {
25019    const installmentText=this.cabanaInstallmentField(lot,field);
25020    if(installmentText!==null)return installmentText;
25021
25022    const fieldNames = [
25023      field?.key,
25024      field?.name,
25025      this.consKey(field?.key),
25026      this.consKey(field?.name),
25027      this.cabanaStoryCleanLabel(field?.name).toLowerCase().replace(/\s+/g, '_'),
25028    ].filter(Boolean);
25029    const formKeys = [
25030      form?.key,
25031      form?.formKey,
25032      form?.name,
25033      this.consKey(form?.key),
25034      this.consKey(form?.name),
25035    ].filter(Boolean);
25036    const buckets = [lot?.values, lot];
25037    formKeys.forEach((formKey) => {
25038      const bucket = lot?.subformValues?.[formKey] || lot?.subformValues?.[this.consKey(formKey)] || lot?.subformValues?.[String(formKey).replace(/-/g, '_')];
25039      if (bucket) buckets.unshift(bucket);
25040    });
25041    for (const bucket of buckets) {
25042      if (!bucket || typeof bucket !== 'object') continue;
25043      for (const name of fieldNames) {
25044        if (bucket[name] !== undefined && bucket[name] !== null && bucket[name] !== '') return bucket[name];
25045        const hit = Object.keys(bucket).find((key) => this.consKey(key) === this.consKey(name));
25046        if (hit && bucket[hit] !== undefined && bucket[hit] !== null && bucket[hit] !== '') return bucket[hit];
25047      }
25048    }
25049    return '';
25050  }
25051  cabanaLotOnCardFacts(lot, remate = null, limit = 4) {
25052    const current = remate || this.state.currentCabana || {};
25053    const lotCategoryKey = lot?.categoryId || lot?.categoryName || lot?.categoriaNombre || lot?.nombre_categoria || lot?.categoria || this.cabanaLotValue(lot, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], '');
25054    const schema = this.cabanaRemateSchema(current).find((category) => this.consKey(category?.categoryId || category?.categoryName || category?.categoria || category?.name) === this.consKey(lotCategoryKey));
25055    if (!schema) return [];
25056    const forms = [
25057      ...((schema.fields || []).length ?[{ name: 'Datos principales', displayOrder: 0, isActive: true, fields: schema.fields }] : []),
25058      ...(schema.forms || []),
25059      ...(schema.subforms || []),
25060    ];
25061    return forms
25062      .filter((form) => form?.isActive !== false)
25063      .sort((a, b) =>
25063 Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0))
25064      .flatMap((form) => (form?.fields || [])
25065        .filter((field) => field?.showOnCard === true || field?.mostrarTarjeta === true)
25066        .sort((a, b) => Number(a?.order || a?.displayOrder || 0) - Number(b?.order || b?.displayOrder || 0))
25067        .map((field) => {
25068          const value = this.cabanaStoryDisplayValue(this.cabanaLotFieldValue(lot, field, form));
25069          return { label: String(field?.name || this.cabanaStoryCleanLabel(field?.key) || 'Dato').toUpperCase(), value };
25070        }))
25071      .filter((fact) => !this.cabanaStoryValueIsEmpty(fact.value))
25072      .slice(0, limit);
25073  }
25074  cabanaLotShareTitle(lot = null) {
25075    const current = this.state.currentCabana || {};
25076    const identity = window.CabanaLotIdentity.resolve(lot, current);
25077    const lotNumber = String(identity?.number || this.cabanaLotNumber(lot) || '').trim();
25078    const lotLabel = String(identity?.label || 'Lote').trim();
25079    const rp = String(this.cabanaAnalyticsRpId(lot) || '').trim();
25080    const auctionTitle = current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña';
25081    return {
25082      lotNumber,
25083      lotLabel,
25084      rp,
25085      title: [lotNumber ?lotLabel + ' ' + lotNumber : lotLabel, rp ?'RP ' + rp : ''].filter(Boolean).join(' - '),
25086      auctionTitle,
25087    };
25088  }
25089  loadCabanaCanvasImage(src) {
25090    return new Promise((resolve) => {
25091      if (!src) return resolve(null);
25092      const image = new Image();
25093      image.crossOrigin = 'anonymous';
25094      image.onload = () => resolve(image);
25095      image.onerror = () => resolve(null);
25096      image.src = this.cabanaCanvasAssetUrl(src);
25097    });
25098  }
25099  cabanaCanvasAssetUrl(src) {
25100    const value = String(src || '').trim();
25101    if (!value || /^(data|blob):/i.test(value)) return value;
25102    if (/^assets\//i.test(value)) return '/' + value;
25103    try {
25104      const url = new URL(value, window.location.href);
25105      if (url.origin === window.location.origin) return url.href;
25106      return '/api/image-proxy?url=' + encodeURIComponent(url.href);
25107    } catch {
25108      return value;
25109    }
25110  }
25111  cabanaStoryMediaUrl(lot = null) {
25112    const images = this.cabanaLotImages(lot);
25113    const videos = this.cabanaLotVideos(lot);
25114    const candidates = [
25115      this.cabanaLotSelectedCoverUrl(lot),
25116      images[0]?.url,
25117      lot?.coverImage?.url,
25118      lot?.coverImage?.downloadUrl,
25119      lot?.cover_image?.url,
25120      this.cabanaLotValue(lot, ['coverImageUrl', 'thumbnailUrl', 'thumbnail', 'imagen', 'imageUrl'], ''),
25121      videos[0]?.thumb,
25122      document.querySelector('.dc-cabana-video-frame img.dc-media-image')?.currentSrc,
25123      document.querySelector('.dc-cabana-lot-player')?.poster,
25124    ];
25125    return window.resolveCabanaMediaUrl(candidates.find((item) => String(item || '').trim()));
25126  }
25127  cabanaStoryCabanaLogoUrl(lot = null, current = this.state.currentCabana || {}) {
25128    const participants = current ?this.cabanaRemateParticipants(current) : [];
25129    const selected = participants.find((cabana) =>
25130      (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
25131      (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) ||
25132      (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) ||
25133      (lot?.cabanaName && cabana.name === lot.cabanaName)
25134    ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
25135    return window.resolveCabanaMediaUrl(lot?.cabanaLogoDataUrl || selected.logoDataUrl || selected.logoUrl || selected.logo || selected.imageUrl || selected.logoWhiteDataUrl || lot?.cabanaLogoWhiteDataUrl);
25136  }
25137  cabanaStoryCabanaLogoNeedsDarkTint(lot = null, current = this.state.currentCabana || {}) {
25138    const participants = current ?this.cabanaRemateParticipants(current) : [];
25139    const selected = participants.find((cabana) =>
25140      (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
25141      (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) ||
25142      (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) ||
25143      (lot?.cabanaName && cabana.name === lot.cabanaName)
25144    ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
25145    const colorLogo = lot?.cabanaLogoDataUrl || selected.logoDataUrl || selected.logoUrl || selected.logo || selected.imageUrl || '';
25146    const whiteLogo = selected.logoWhiteDataUrl || lot?.cabanaLogoWhiteDataUrl || '';
25147    return !colorLogo && !!whiteLogo;
25148  }
25149  drawCabanaRoundRect(ctx, x, y, width, height, radius) {
25150    const r = Math.min(radius, width / 2, height / 2);
25151    ctx.beginPath();
25152    ctx.moveTo(x + r, y);
25153    ctx.arcTo(x + width, y, x + width, y + height, r);
25154    ctx.arcTo(x + width, y + height, x, y + height, r);
25155    ctx.arcTo(x, y + height, x, y, r);
25156    ctx.arcTo(x, y, x + width, y, r);
25157    ctx.closePath();
25158  }
25159  drawCabanaImageCover(ctx, image, x, y, width, height, radius = 0) {
25160    if (!image) return;
25161    const scale = Math.max(width / image.naturalWidth, height / image.naturalHeight);
25162    const sw = width / scale;
25163    const sh = height / scale;
25164    const sx = (image.naturalWidth - sw) / 2;
25165    const sy = (image.naturalHeight - sh) / 2;
25166    ctx.save();
25167    if (radius) {
25168      this.drawCabanaRoundRect(ctx, x, y, width, height, radius);
25169      ctx.clip();
25170    }
25171    ctx.drawImage(image, sx, sy, sw, sh, x, y, width, height);
25172    ctx.restore();
25173  }
25174  drawCabanaImageContain(ctx, image, x, y, width, height, filter = '') {
25175    if (!image) return;
25176    const scale = Math.min(width / image.naturalWidth, height / image.naturalHeight);
25177    const dw = image.naturalWidth * scale;
25178    const dh = image.naturalHeight * scale;
25179    ctx.save();
25180    if (filter) ctx.filter = filter;
25181    ctx.drawImage(image, x + (width - dw) / 2, y + (height - dh) / 2, dw, dh);
25182    ctx.restore();
25183  }
25184  drawCabanaImageTintContain(ctx, image, x, y, width, height, color = '#ffffff') {
25185    if (!image) return;
25186    const scale = Math.min(width / image.naturalWidth, height / image.naturalHeight);
25187    const dw = image.naturalWidth * scale;
25188    const dh = image.naturalHeight * scale;
25189    const dx = x + (width - dw) / 2;
25190    const dy = y + (height - dh) / 2;
25191    const mask = document.createElement('canvas');
25192    mask.width = Math.max(1, Math.ceil(dw));
25193    mask.height = Math.max(1, Math.ceil(dh));
25194    const maskCtx = mask.getContext('2d');
25195    maskCtx.drawImage(image, 0, 0, mask.width, mask.height);
25196    maskCtx.globalCompositeOperation = 'source-in';
25197    maskCtx.fillStyle = color;
25198    maskCtx.fillRect(0, 0, mask.width, mask.height);
25199    ctx.drawImage(mask, dx, dy, dw, dh);
25200  }
25201  drawCabanaImageStoryFit(ctx, image, x, y, maxWidth, maxHeight, radius = 0) {
25202    if (!image) return { x, y, width: maxWidth, height: maxHeight, bottom: y + maxHeight };
25203    const scale = Math.min(maxWidth / image.naturalWidth, maxHeight / image.naturalHeight);
25204    const width = image.naturalWidth * scale;
25205    const height = image.naturalHeight * scale;
25206    const dx = x + (maxWidth - width) / 2;
25207    ctx.save();
25208    if (radius) {
25209      this.drawCabanaRoundRect(ctx, dx, y, width, height, radius);
25210      ctx.clip();
25211    }
25212    ctx.drawImage(image, dx, y, width, height);
25213    ctx.restore();
25214    return { x: dx, y, width, height, bottom: y + height };
25215  }
25216  wrapCabanaCanvasText(ctx, text, x, y, maxWidth, lineHeight, maxLines = 3) {
25217    const words = String(text || '').split(/\s+/).filter(Boolean);
25218    const lines = [];
25219    let line = '';
25220    words.forEach((word) => {
25221      const test = line ?line + ' ' + word : word;
25222      if (ctx.measureText(test).width > maxWidth && line) {
25223        lines.push(line);
25224        line = word;
25225      } else {
25226        line = test;
25227      }
25228    });
25229    if (line) lines.push(line);
25230    const visible = lines.slice(0, maxLines);
25231    if (lines.length > maxLines) {
25232      let last = visible[visible.length - 1] || '';
25233      while (ctx.measureText(last + '...').width > maxWidth && last.length > 0) last = last.slice(0, -1);
25234      visible[visible.length - 1] = last.trim() + '...';
25235    }
25236    visible.forEach((row, index) => ctx.fillText(row, x, y + index * lineHeight));
25237    return y + visible.length * lineHeight;
25238  }
25239  async createCabanaLotStoryBlob(lot = null, context = {}) {
25240    const target = lot || this.state.currentCabanaLot;
25241    if (!target) throw new Error('lot-missing');
25242    const current = context?.remate || this.state.currentCabana || {};
25243    const { lotNumber, lotLabel, rp, auctionTitle } = this.cabanaLotShareTitle(target);
25244    const category = String(target?.categoryName || target?.categoriaNombre || this.cabanaLotValue(target, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], 'Reproductor'));
25245    const participants = current ?this.cabanaRemateParticipants(current) : [];
25246    const selectedCabana = participants.find((cabana) =>
25247      (target?.cabanaKey && cabana.key === target.cabanaKey) ||
25248      (target?.cabanaId && String(cabana.id || '') === String(target.cabanaId)) ||
25249      (target?.cabanaNombre && cabana.name === target.cabanaNombre) ||
25250      (target?.cabanaName && cabana.name === target.cabanaName)
25251    ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
25252    const cabanaName = String(target?.cabanaNombre || target?.cabanaName || selectedCabana?.name || selectedCabana?.nombre || auctionTitle || 'Rosgan');
25253    const mediaUrl = this.cabanaStoryMediaUrl(target);
25254    const mainLogoUrl = this.cabanaStoryCabanaLogoUrl(target, current);
25255    const mainLogoNeedsDarkTint = this.cabanaStoryCabanaLogoNeedsDarkTint(target, current);
25256    const rosganLogoUrl = '/assets/rosgan-logo-blue.png';
25257    const consignatariaRecord = (this.state.consignatarias || []).find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(current?.consignatariaId || current?.consignataria || current?.nombre_cuenta)));
25258    const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || consignatariaRecord?.slug || '';
25259    const consignatariaName = (consignatariaRecord ?this.consName(consignatariaRecord) : '') || current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Rosgan';
25260    const isLaCassina = this.state.publicAdvanceMode === 'lacassina' || /cassina/i.test(String(auctionTitle || current?.nombre_remate || current?.titulo || current?.title || ''));
25261    let consignatariaLogoUrl = this.consWhiteLogo(consignatariaRaw || consignatariaName || current?.nombre_cuenta || '') || this.consLogo(consignatariaRaw || consignatariaName || '') || consignatariaRecord?.logoUrl || consignatariaRecord?.logo || consignatariaRecord?.imageUrl || '';
25262    if (isLaCassina || /colombo/i.test(consignatariaName)) consignatariaLogoUrl = '/assets/consignatarias-white/colombo-y-colombo-s-a.png';
25263    const consignatariaColor = (isLaCassina || /colombo/i.test(consignatariaName)) ?'#D71945' : this.consBrandColor(consignatariaRaw || consignatariaName || 'Rosgan');
25264    const consignatariaColorDeep = (isLaCassina || /colombo/i.test(consignatariaName)) ?'#A80F35' : this.shade(consignatariaColor, -0.2);
25265    const lotContact = context?.contact || this.cabanaContactData(current);
25266    const lotContactName = lotContact.name || 'Equipo Rosgan';
25267    const lotContactPhone = lotContact.phone || 'rosgan.app';
25268    const [mediaImage, mainLogo, rosganLogo, consignatariaLogo, faviconLogo, storyBackground] = await Promise.all([
25269      this.loadCabanaCanvasImage(mediaUrl),
25270      this.loadCabanaCanvasImage(mainLogoUrl),
25271      this.loadCabanaCanvasImage(rosganLogoUrl),
25272      this.loadCabanaCanvasImage(consignatariaLogoUrl),
25273      this.loadCabanaCanvasImage('/favicon.png'),
25274      isLaCassina ?this.loadCabanaCanvasImage('/assets/lacassina-story-background.png') : Promise.resolve(null),
25275    ]);
25276    const canvas = document.createElement('canvas');
25277    canvas.width = 1080;
25278    canvas.height = 1920;
25279    const ctx = canvas.getContext('2d');
25280    ctx.fillStyle = '#ffffff';
25281    ctx.fillRect(0, 0, 1080, 1920);
25282    ctx.fillStyle = '#ffffff';
25283    this.drawCabanaRoundRect(ctx, 54, 86, 972, 1738, 42);
25284    ctx.fill();
25285    if (storyBackground) {
25286      ctx.save();
25287      ctx.globalAlpha = 1;
25288      this.drawCabanaImageCover(ctx, storyBackground, 54, 86, 972, 1738, 42);
25289      ctx.restore();
25290    } else {
25291      const bg = ctx.createLinearGradient(54, 86, 1026, 1824);
25292      bg.addColorStop(0, '#e8f6ff');
25293      bg.addColorStop(0.56, '#f4d39b');
25294      bg.addColorStop(1, '#f6fafc');
25295      ctx.fillStyle = bg;
25296      this.drawCabanaRoundRect(ctx, 54, 86, 972, 1738, 42);
25297      ctx.fill();
25298    }
25299    ctx.strokeStyle = '#d8e4ef';
25300    ctx.lineWidth = 2;
25301    this.drawCabanaRoundRect(ctx, 54, 86, 972, 1738, 42);
25302    ctx.stroke();
25303    if (rosganLogo) this.drawCabanaImageContain(ctx, rosganLogo, 82, 190, 246, 76);
25304    else {
25305      ctx.fillStyle = '#17304d';
25306      ctx.font = '900 38px Archivo, Arial, sans-serif';
25307      ctx.fillText('ROSGAN', 82, 238);
25308    }
25309    if (mainLogo) {
25310      if (mainLogoNeedsDarkTint) this.drawCabanaImageTintContain(ctx, mainLogo, 610, 174, 388, 132, '#17304d');
25311      else this.drawCabanaImageContain(ctx, mainLogo, 610, 174, 388, 132);
25312    }
25313    ctx.font = '900 24px Archivo, Arial, sans-serif';
25314    ctx.fillStyle = '#073763';
25315    ctx.fillText('REMATES DE CABANAS', 82, 370);
25316    ctx.strokeStyle = '#073763';
25317    ctx.lineWidth = 2;
25318    ctx.beginPath();
25319    ctx.moveTo(82, 400);
25320    ctx.lineTo(998, 400);
25321    ctx.stroke();
25322    let mediaBox = { x: 82, y: 436, width: 916, height: 600, bottom: 1036 };
25323    if (mediaImage) {
25324      mediaBox = this.drawCabanaImageStoryFit(ctx, mediaImage, 82, 436, 916, 600, 28);
25325    } else {
25326      const ph = ctx.createLinearGradient(82, 436, 998, 1036);
25327      ph.addColorStop(0, '#12375c');
25328      ph.addColorStop(1, '#0d8fb2');
25329      ctx.fillStyle = ph;
25330      this.drawCabanaRoundRect(ctx, 82, 436, 916, 600, 30);
25331      ctx.fill();
25332      ctx.fillStyle = 'rgba(255,255,255,.86)';
25333      ctx.font = '900 74px Archivo, Arial, sans-serif';
25334      ctx.fillText(rp ?'RP ' + rp : 'Rosgan', 132, 780);
25335    }
25336    ctx.strokeStyle = '#d7e3ef';
25337    ctx.lineWidth = 2;
25338    this.drawCabanaRoundRect(ctx, mediaBox.x, mediaBox.y, mediaBox.width, mediaBox.height, 28);
25339    ctx.stroke();
25340    const imageFade = ctx.createLinearGradient(mediaBox.x, mediaBox.bottom - 324, mediaBox.x, mediaBox.bottom);
25341    imageFade.addColorStop(0, 'rgba(0,0,0,0)');
25342    imageFade.addColorStop(1, 'rgba(0,24,48,.78)');
25343    ctx.fillStyle = imageFade;
25344    this.drawCabanaRoundRect(ctx, mediaBox.x, mediaBox.y, mediaBox.width, mediaBox.height, 28);
25345    ctx.fill();
25346    ctx.fillStyle = '#ffffff';
25347    ctx.font = '900 66px Archivo, Arial, sans-serif';
25348    const lotTitle = lotNumber ?lotLabel + ' ' + lotNumber : lotLabel;
25349    ctx.fillText(lotTitle, 132, mediaBox.bottom - 144);
25350    ctx.font = '900 40px Archivo, Arial, sans-serif';
25351    ctx.fillText(rp ?'RP ' + rp : 'RP', 132, mediaBox.bottom - 90);
25352    ctx.font = '800 25px Archivo, Arial, sans-serif';
25353    ctx.fillStyle = 'rgba(255,255,255,.86)';
25354    this.wrapCabanaCanvasText(ctx, category, 132, mediaBox.bottom - 50, 760, 33, 2);
25355    const detailY = mediaBox.bottom + 48;
25356    const detailPanelH = 304;
25357    ctx.fillStyle = 'rgba(254,251,246,.9)';
25358    this.drawCabanaRoundRect(ctx, 82, detailY, 916, detailPanelH, 30);
25359    ctx.fill();
25360    ctx.strokeStyle = 'rgba(253,250,245,.72)';
25361    ctx.lineWidth = 2;
25362    this.drawCabanaRoundRect(ctx, 82, detailY, 916, detailPanelH, 30);
25363    ctx.stroke();
25364    ctx.textAlign = 'center';
25365    ctx.fillStyle = '#17304d';
25366    ctx.font = '900 34px Archivo, Arial, sans-serif';
25367    ctx.fillText(cabanaName, 540, detailY + 60);
25368    ctx.fillStyle = '#6b7b90';
25369    ctx.font = '800 21px Archivo, Arial, sans-serif';
25370    ctx.fillText(auctionTitle, 540, detailY + 106);
25371    ctx.strokeStyle = '#e0e9f2';
25372    ctx.lineWidth = 1.5;
25373    ctx.beginPath();
25374    ctx.moveTo(154, detailY + 142);
25375    ctx.lineTo(926, detailY + 142);
25376    ctx.stroke();
25377    const onCardFacts = this.cabanaLotOnCardFacts(target, current, 4);
25378    const fallbackFacts = [
25379      ['N', this.cabanaLotValue(target, ['numero', 'nro', 'nro_animal', 'orden_final'], '')],
25380      ['Caravana', this.cabanaLotValue(target, ['caravana', 'caravanaNumero', 'caravana_numero'], '')],
25381      ['Padre', this.cabanaLotValue(target, ['padre', 'nombrePadre'], '')],
25382      ['Nacimiento', this.cabanaLotValue(target, ['nacimiento', 'fecha_nacimiento', 'anio_nacimiento'], '')],
25383      ['C. escrotal', this.cabanaLotValue(target, ['ce', 'circunferenciaEscrotal'], '')],
25384      ['Peso', this.cabanaLotValue(target, ['peso', 'pesoPromedio', 'peso_promedio'], '')],
25385    ].filter(([, value]) => String(value || '').trim()).slice(0, 4).map(([label, value]) => ({ label, value }));
25386    const facts = onCardFacts.length ?onCardFacts : fallbackFacts;
25387    facts.forEach((fact, index) => {
25388      const { label, value } = fact;
25389      const y = detailY + 176 + index * 38;
25390      if (index > 0) {
25391        ctx.strokeStyle = 'rgba(23,48,77,.18)';
25392        ctx.lineWidth = 1;
25393        ctx.beginPath();
25394        ctx.moveTo(126, y - 22);
25395        ctx.lineTo(954, y - 22);
25396        ctx.stroke();
25397      }
25398      ctx.textAlign = 'left';
25399      ctx.fillStyle = '#7d8da3';
25400      ctx.font = '900 18px Archivo, Arial, sans-serif';
25401      ctx.fillText(String(label).toUpperCase(), 126, y);
25402      ctx.textAlign = 'right';
25403      ctx.fillStyle = '#0b315d';
25404      ctx.font = '900 25px Archivo, Arial, sans-serif';
25405      ctx.fillText(String(value), 954, y + 2);
25406    });
25407    ctx.textAlign = 'left';
25408    const consignatariaPanelY = detailY + detailPanelH + 34;
25409    const consignatariaPanelH = 284;
25410    const consPanel = ctx.createLinearGradient(82, consignatariaPanelY, 998, consignatariaPanelY + consignatariaPanelH);
25411    consPanel.addColorStop(0, consignatariaColor);
25412    consPanel.addColorStop(1, consignatariaColorDeep);
25413    ctx.save();
25414    ctx.globalAlpha = 0.94;
25415    ctx.fillStyle = consPanel;
25416    this.drawCabanaRoundRect(ctx, 82, consignatariaPanelY, 916, consignatariaPanelH, 30);
25417    ctx.fill();
25418    ctx.restore();
25419    ctx.strokeStyle = 'rgba(255,255,255,.22)';
25420    ctx.lineWidth = 1.5;
25421    this.drawCabanaRoundRect(ctx, 82, consignatariaPanelY, 916, consignatariaPanelH, 30);
25422    ctx.stroke();
25423    if (consignatariaLogo) this.drawCabanaImageTintContain(ctx, consignatariaLogo, 144, consignatariaPanelY + 52, 104, 96);
25424    ctx.textAlign = 'center';
25425    ctx.fillStyle = 'rgba(255,255,255,.66)';
25426    ctx.font = '900 18px Archivo, Arial, sans-serif';
25427    ctx.fillText('CONSIGNATARIA', 620, consignatariaPanelY + 68);
25428    ctx.fillStyle = '#ffffff';
25429    ctx.font = '900 38px Archivo, Arial, sans-serif';
25430    ctx.fillText(consignatariaName, 620, consignatariaPanelY + 118);
25431    ctx.strokeStyle = 'rgba(255,255,255,.24)';
25432    ctx.beginPath();
25433    ctx.moveTo(126, consignatariaPanelY + 164);
25434    ctx.lineTo(954, consignatariaPanelY + 164);
25435    ctx.stroke();
25436    ctx.fillStyle = 'rgba(255,255,255,.72)';
25437    ctx.font = '900 17px Archivo, Arial, sans-serif';
25438    ctx.fillText('CONTACTO DEL LOTE', 310, consignatariaPanelY + 206);
25439    ctx.fillText('TELEFONO', 770, consignatariaPanelY + 206);
25440    ctx.fillStyle = '#ffffff';
25441    ctx.font = '900 28px Archivo, Arial, sans-serif';
25442    ctx.fillText(lotContactName, 310, consignatariaPanelY + 240);
25443    ctx.font = '900 27px Archivo, Arial, sans-serif';
25444    ctx.fillText(lotContactPhone, 770, consignatariaPanelY + 240);
25445    ctx.textAlign = 'left';
25446    ctx.fillStyle = '#17304d';
25447    ctx.font = '900 32px Archivo, Arial, sans-serif';
25448    ctx.shadowColor = 'transparent';
25449    ctx.shadowBlur = 0;
25450    ctx.shadowOffsetY = 0;
25451    const footerText = 'rosgan.app';
25452    const footerIconSize = faviconLogo ?50 : 0;
25453    const footerGap = faviconLogo ?14 : 0;
25454    const footerTextWidth = ctx.measureText(footerText).width;
25455    const footerTotalWidth = footerIconSize + footerGap + footerTextWidth;
25456    const footerX = 540 - footerTotalWidth / 2;
25457    const footerY = 1708;
25458    if (faviconLogo) this.drawCabanaImageContain(ctx, faviconLogo, footerX, footerY - 39, footerIconSize, footerIconSize);
25459    ctx.fillText(footerText, footerX + footerIconSize + footerGap, footerY);
25460    ctx.shadowColor = 'transparent';
25461    ctx.shadowBlur = 0;
25462    ctx.shadowOffsetY = 0;
25463    return await new Promise((resolve, reject) => {
25464      try {
25465        canvas.toBlob((blob) => blob ?resolve(blob) : reject(new Error('canvas-empty')), 'image/png', 0.94);
25466      } catch (error) {
25467        reject(error);
25468      }
25469    });
25470  }
25471  async createCabanaPriceBoardStoryBlob(rows = [], rosgan = null) {
25472    const visibleRows = (Array.isArray(rows) ?rows : []).filter((row) => rosgan || Number(row?.count || 0) > 0);
25473    if (!visibleRows.length) throw new Error('No hay precios vendidos para exportar.');
25474    const current = rosgan?.remate || this.state.currentCabana || {};
25475    const auctionTitle = current?.nombre_remate || current?.titulo || current?.title || 'La Cassina';
25476    const participants = rosgan?[]:this.cabanaRemateParticipants(current);
25477    const mainParticipant = participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
25478    const mainLogoUrl = mainParticipant.logoDataUrl || mainParticipant.logoUrl || mainParticipant.logo || mainParticipant.imageUrl || '';
25479    const rosganLogoUrl = '/assets/rosgan-logo-blue.png';
25480    const consignatariaRecord = (this.state.consignatarias || []).find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(current?.consignatariaId || current?.consignataria || current?.nombre_cuenta)));
25481    const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || consignatariaRecord?.slug || '';
25482    const consignatariaName = rosgan?'Mercado Ganadero · Rosgan':(consignatariaRecord ?this.consName(consignatariaRecord) : '') || current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Colombo y Colombo S.A.';
25483    let consignatariaLogoUrl = this.consWhiteLogo(consignatariaRaw || consignatariaName) || this.consLogo(consignatariaRaw || consignatariaName) || consignatariaRecord?.logoUrl || consignatariaRecord?.logo || '';
25484    if (/cassina/i.test(auctionTitle) || /colombo/i.test(consignatariaName)) consignatariaLogoUrl = '/assets/consignatarias-white/colombo-y-colombo-s-a.png';
25485    const [mainLogo, rosganLogo, consignatariaLogo, faviconLogo, storyBackground] = await Promise.all([
25486      this.loadCabanaCanvasImage(mainLogoUrl),
25487      this.loadCabanaCanvasImage(rosganLogoUrl),
25488      this.loadCabanaCanvasImage(consignatariaLogoUrl),
25489      this.loadCabanaCanvasImage('/favicon.png'),
25490      rosgan?Promise.resolve(null):this.loadCabanaCanvasImage('/assets/lacassina-story-background.png'),
25491    ]);
25492    const canvas = document.createElement('canvas');
25493    canvas.width = 1080;
25494    canvas.height = 1920;
25495    const ctx = canvas.getContext('2d');
25496    ctx.fillStyle = '#ffffff';
25497    ctx.fillRect(0, 0, canvas.width, canvas.height);
25498    if (storyBackground) this.drawCabanaImageCover(ctx, storyBackground, 54, 54, 972, 1812, 42);
25499    else {
25500      const fallback = ctx.createLinearGradient(54, 54, 1026, 1866);
25501      fallback.addColorStop(0, '#fffaf2');
25502      fallback.addColorStop(0.55, '#f7fbff');
25503      fallback.addColorStop(1, '#e6f3fb');
25504      ctx.fillStyle = fallback;
25505      this.drawCabanaRoundRect(ctx, 54, 54, 972, 1812, 42);
25506      ctx.fill();
25507    }
25508    ctx.strokeStyle = '#d6e3ee';
25509    ctx.lineWidth = 2;
25510    this.drawCabanaRoundRect(ctx, 54, 54, 972, 1812, 42);
25511    ctx.stroke();
25512
25513    if (rosgan) {
25514      const header = ctx.createLinearGradient(54, 54, 1026, 280);
25515      header.addColorStop(0, '#052e51'); header.addColorStop(1, '#096487');
25516      ctx.fillStyle = header; this.drawCabanaRoundRect(ctx, 54, 54, 972, 218, 32); ctx.fill();
25517      if (rosganLogo) this.drawCabanaImageTintContain(ctx, rosganLogo, 88, 94, 218, 66, '#ffffff');
25518      ctx.fillStyle = '#ffffff'; ctx.font = '900 34px Archivo, Arial, sans-serif';
25519      ctx.fillText('RESULTADOS DEL REMATE', 338, 137);
25520      ctx.font = '700 24px Archivo, Arial, sans-serif';
25521      this.wrapCabanaCanvasText(ctx, auctionTitle, 84, 211, 890, 28, 1);
25522      const auctionDate = this.remateDate(current);
25523      ctx.fillStyle = '#bce7ec'; ctx.font = '600 20px Archivo, Arial, sans-serif';
25524      ctx.fillText(auctionDate ? auctionDate.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric', timeZone: 'America/Argentina/Buenos_Aires' }) : 'Fecha a confirmar', 84, 245);
25525      ctx.fillStyle = '#48677d'; ctx.font = '800 18px Archivo, Arial, sans-serif';
25526      ctx.fillText('CATEGORÍA', 104, 317);
25527      ctx.textAlign = 'center'; ctx.fillText('MÁXIMO', 597, 317);
25528      ctx.fillStyle = '#08796e'; ctx.fillText('PROMEDIO', 857, 317); ctx.textAlign = 'left';
25529    } else {
25530    if (rosganLogo) this.drawCabanaImageContain(ctx, rosganLogo, 86, 108, 242, 78);
25531    else {
25532      ctx.fillStyle = '#17304d';
25533      ctx.font = '900 38px Archivo, Arial, sans-serif';
25534      ctx.fillText('ROSGAN', 86, 160);
25535    }
25536    if (mainLogo) this.drawCabanaImageContain(ctx, mainLogo, 624, 86, 372, 122);
25537
25538    const topConsPanel = ctx.createLinearGradient(82, 230, 998, 340);
25539    topConsPanel.addColorStop(0, rosgan?'#002e5f':'#D71945');
25540    topConsPanel.addColorStop(1, rosgan?'#075995':'#A80F35');
25541    ctx.fillStyle = topConsPanel;
25542    this.drawCabanaRoundRect(ctx, 82, 230, 916, 110, 26);
25543    ctx.fill();
25544    if (consignatariaLogo) this.drawCabanaImageTintContain(ctx, consignatariaLogo, 128, 254, 96, 62);
25545    ctx.fillStyle = 'rgba(255,255,255,.68)';
25546    ctx.font = '900 15px Archivo, Arial, sans-serif';
25547    ctx.fillText(rosgan?'REMATES':'CONSIGNATARIA', 270, 268);
25548    ctx.fillStyle = '#ffffff';
25549    ctx.font = '900 32px Archivo, Arial, sans-serif';
25550    ctx.fillText(consignatariaName, 270, 310);
25551
25552    ctx.fillStyle = '#073763';
25553    this.drawCabanaRoundRect(ctx, 82, 366, 916, 220, 32);
25554    ctx.fill();
25555    ctx.fillStyle = '#d8c49b';
25556    ctx.font = '900 20px Archivo, Arial, sans-serif';
25557    ctx.letterSpacing = '3px';
25558    ctx.fillText('ROSGAN', 126, 415);
25559    ctx.letterSpacing = '0px';
25560    ctx.fillStyle = '#ffffff';
25561    ctx.font = '900 51px Archivo, Arial, sans-serif';
25562    ctx.fillText('PLACA DE PRECIOS', 126, 469);
25563    ctx.fillStyle = 'rgba(255,255,255,.84)';
25564    ctx.font = '800 22px Archivo, Arial, sans-serif';
25565    this.wrapCabanaCanvasText(ctx, auctionTitle, 126, 510, 820, 29, 2);
25566    ctx.fillStyle = 'rgba(255,255,255,.68)';
25567    ctx.font = '800 16px Archivo, Arial, sans-serif';
25568    ctx.fillText('RESULTADOS POR CATEGORIA', 126, 559);
25569    }
25570    const cardsTop = rosgan ? 338 : 620;
25571    const cardsBottom = 1760;
25572    const gap = rosgan ? 4 : 12;
25573    const compactGrid = !rosgan && visibleRows.length > 9;
25574    const columns = compactGrid ?2 : 1;
25575    const rowCount = Math.ceil(visibleRows.length / columns);
25576    const columnGap = compactGrid ?14 : 0;
25577    const cardWidth = Math.floor((916 - columnGap * (columns - 1)) / columns);
25578    const cardHeight = Math.max(rosgan ? 0 : 84, Math.min(rosgan ? 156 : 108, Math.floor((cardsBottom - cardsTop - gap * (rowCount - 1)) / rowCount)));
25579    const money = (value) => rosgan && value == null ? '—' : '$' + new Intl.NumberFormat('es-AR', { maximumFractionDigits: 0 }).format(Number(value || 0));
25580    visibleRows.forEach((row, index) => {
25581      const columnIndex = compactGrid ?index % columns : 0;
25582      const rowIndex = compactGrid ?Math.floor(index / columns) : index;
25583      const isLastUnpaired = compactGrid && visibleRows.length % 2 === 1 && index === visibleRows.length - 1;
25584      const x = isLastUnpaired ?82 + Math.floor((916 - cardWidth) / 2) : 82 + columnIndex * (cardWidth + columnGap);
25585      const y = cardsTop + rowIndex * (cardHeight + gap);
25586      if (rosgan) {
25587        ctx.fillStyle = index % 2 ? '#edf3f7' : '#ffffff';
25588        this.drawCabanaRoundRect(ctx, 82, y, 916, cardHeight, 10); ctx.fill();
25589        ctx.fillStyle = '#e0f1ee';
25590        this.drawCabanaRoundRect(ctx, 732, y + 6, 254, cardHeight - 12, 9); ctx.fill();
25591        ctx.fillStyle = '#082f5c';
25592        ctx.font = '800 32px Archivo, Arial, sans-serif';
25593        const categoryLines = ctx.measureText(row.category).width > 360 ? 2 : 1;
25594        this.wrapCabanaCanvasText(ctx, row.category, 104, y + cardHeight / 2 + 11 - (categoryLines - 1) * 18, 360, 36, 2);
25595        for (const [label, value, center] of [['MÁXIMO', row.maxValue, 597], ['PROMEDIO', row.avgValue, 857]]) {
25596          ctx.textAlign = 'center';
25597          const text = money(value);
25598          let size = 36;
25599          ctx.font = `900 ${size}px Archivo, Arial, sans-serif`;
25600          while (ctx.measureText(text).width > 238 && size > 24) ctx.font = `900 ${--size}px Archivo, Arial, sans-serif`;
25601          ctx.fillStyle = value == null ? '#9ab0bf' : label === 'PROMEDIO' ? '#09695f' : '#073763';
25602          ctx.fillText(text, center, y + cardHeight / 2 + size * .34);
25603        }
25604        ctx.textAlign = 'left';
25605        return;
25606      }
25607      if (compactGrid) {
25608        ctx.save();
25609        ctx.shadowColor = 'rgba(15,42,70,.09)';
25610        ctx.shadowBlur = 12;
25611        ctx.shadowOffsetY = 4;
25612        ctx.fillStyle = 'rgba(255,255,255,.97)';
25613        this.drawCabanaRoundRect(ctx, x, y, cardWidth, cardHeight, 18);
25614        ctx.fill();
25615        ctx.restore();
25616        ctx.strokeStyle = 'rgba(7,55,99,.13)';
25617        ctx.lineWidth = 1.5;
25618        this.drawCabanaRoundRect(ctx, x, y, cardWidth, cardHeight, 18);
25619        ctx.stroke();
25620        const splitX = x + Math.floor(cardWidth * .49);
25621        ctx.strokeStyle = 'rgba(7,55,99,.10)';
25622        ctx.beginPath();
25623        ctx.moveTo(splitX, y + 14);
25624        ctx.lineTo(splitX, y + cardHeight - 14);
25625        ctx.stroke();
25626        ctx.fillStyle = '#6e8198';
25627        ctx.font = '900 9px Archivo, Arial, sans-serif';
25628        ctx.fillText('CATEGORIA', x + 18, y + 21);
25629        ctx.fillStyle = '#082f5c';
25630        ctx.font = `900 ${cardHeight >= 100 ?16 : 14}px Archivo, Arial, sans-serif`;
25631        this.wrapCabanaCanvasText(ctx, row.category, x + 18, y + 43, splitX - x - 32, cardHeight >= 100 ?18 : 16, 3);
25632        const metricX = splitX + 16;
25633        ctx.fillStyle = '#6e8198';
25634        ctx.font = '900 9px Archivo, Arial, sans-serif';
25635        ctx.fillText('PROMEDIO', metricX, y + 21);
25636        ctx.fillStyle = '#073763';
25637        ctx.font = `900 ${cardHeight >= 100 ?22 : 19}px Archivo, Arial, sans-serif`;
25638        ctx.fillText(money(row.avgValue), metricX, y + 47);
25639        ctx.fillStyle = '#6e8198';
25640        ctx.font = '900 9px Archivo, Arial, sans-serif';
25641        ctx.fillText('MAXIMO', metricX, y + cardHeight - 31);
25642        ctx.fillStyle = '#405b78';
25643        ctx.font = `900 ${cardHeight >= 100 ?17 : 15}px Archivo, Arial, sans-serif`;
25644        ctx.fillText(money(row.maxValue), metricX, y + cardHeight - 12);
25645        return;
25646      }
25647      ctx.save();
25648      ctx.shadowColor = 'rgba(15,42,70,.10)';
25649      ctx.shadowBlur = 14;
25650      ctx.shadowOffsetY = 5;
25651      ctx.fillStyle = 'rgba(255,255,255,.96)';
25652      this.drawCabanaRoundRect(ctx, 82, y, 916, cardHeight, 19);
25653      ctx.fill();
25654      ctx.restore();
25655      ctx.strokeStyle = 'rgba(7,55,99,.11)';
25656      ctx.lineWidth = 1.5;
25657      this.drawCabanaRoundRect(ctx, 82, y, 916, cardHeight, 19);
25658      ctx.stroke();
25659      ctx.fillStyle = '#6e8198';
25660      ctx.font = '900 11px Archivo, Arial, sans-serif';
25661      ctx.fillText('CATEGORIA', 112, y + 25);
25662      ctx.fillStyle = '#082f5c';
25663      const categoryFont = cardHeight >= 100 ?20 : 17;
25664      const categoryLine = cardHeight >= 100 ?22 : 19;
25665      ctx.font = `900 ${categoryFont}px Archivo, Arial, sans-serif`;
25666      this.wrapCabanaCanvasText(ctx, row.category, 112, y + 51, 366, categoryLine, 3);
25667      const priceX = 510;
25668      const priceY = y + 9;
25669      const priceW = 452;
25670      const priceH = cardHeight - 18;
25671      ctx.fillStyle = '#073763';
25672      this.drawCabanaRoundRect(ctx, priceX, priceY, priceW, priceH, 15);
25673      ctx.fill();
25674      ctx.fillStyle = 'rgba(255,255,255,.66)';
25675      ctx.font = '900 10px Archivo, Arial, sans-serif';
25676      ctx.fillText('PRECIO PROMEDIO', priceX + 24, priceY + 22);
25677      ctx.fillStyle = '#ffffff';
25678      ctx.font = `900 ${cardHeight >= 100 ?29 : 25}px Archivo, Arial, sans-serif`;
25679      ctx.fillText(money(row.avgValue), priceX + 24, priceY + 54);
25680      ctx.strokeStyle = 'rgba(255,255,255,.18)';
25681      ctx.lineWidth = 1;
25682      ctx.beginPath();
25683      ctx.moveTo(priceX + 24, priceY + priceH - 30);
25684      ctx.lineTo(priceX + priceW - 24, priceY + priceH - 30);
25685      ctx.stroke();
25686      ctx.fillStyle = 'rgba(255,255,255,.72)';
25687      ctx.font = '800 10px Archivo, Arial, sans-serif';
25688      ctx.fillText('MAXIMO', priceX + 24, priceY + priceH - 10);
25689      ctx.textAlign = 'right';
25690      ctx.fillStyle = '#ffffff';
25691      ctx.font = '900 16px Archivo, Arial, sans-serif';
25692      ctx.fillText(money(row.maxValue), priceX + priceW - 24, priceY + priceH - 9);
25693      ctx.textAlign = 'left';
25694    });
25695
25696    ctx.fillStyle = '#17304d';
25697    ctx.font = '900 28px Archivo, Arial, sans-serif';
25698    const footer = 'rosgan.app';
25699    const footerWidth = ctx.measureText(footer).width;
25700    const iconSize = faviconLogo ?42 : 0;
25701    const footerX = 540 - (footerWidth + iconSize + (iconSize ?12 : 0)) / 2;
25702    if (faviconLogo) this.drawCabanaImageContain(ctx, faviconLogo, footerX, 1810, iconSize, iconSize);
25703    ctx.fillText(footer, footerX + iconSize + (iconSize ?12 : 0), 1842);
25704    return await new Promise((resolve, reject) => {
25705      try { canvas.toBlob((blob) => blob ?resolve(blob) : reject(new Error('canvas-empty')), 'image/png', 0.94); }
25706      catch (error) { reject(error); }
25707    });
25708  }
25709  async downloadRosganPriceStory(rows,model){
25710    if(this.state.rosganPriceStoryBusy)return;
25711    if(!rows?.length){this.showToast('Para generar la Story, cargá al menos un lote vendido con precio en Comercial.');return;}
25712    this.setState({rosganPriceStoryBusy:true,rosganPriceExportMessage:'Generando imagen…'});
25713    try{const pages=window.RosganPriceBoard.pages(model);for(let i=0;i<pages.length;i++){
25714      const blob=await window.RosganPriceBoard.png({...pages[i],pageLabel:pages.length>1?(i+1)+' / '+pages.length:''});
25715      this.downloadCabanaBlob(blob,this.cabanaExportFileBase(this.state.currentRemate)+'-precios-'+model.mode+'-'+(i+1)+'.png');
25716    }this.setState({rosganPriceExportMessage:'Placa de precios descargada.'});this.showToast('Placa de precios descargada.');}
25717    catch(error){this.setState({rosganPriceExportMessage:error.message||'No se pudo generar la placa.'});this.showToast(error.message||'No se pudo generar la placa.');}
25718    finally{this.setState({rosganPriceStoryBusy:false});}
25719  }
25720  async downloadCabanaPriceBoardStory(rows = [], model = null) {
25721    if (this.state.cabanaPriceStoryBusy) return;
25722    this.setState({ cabanaPriceStoryBusy: true });
25723    try {
25724      const allRows = (Array.isArray(rows) ?rows : []).filter((row) => Number(row?.count || 0) > 0);
25725      if (!allRows.length) throw new Error('No hay precios vendidos para exportar.');
25726      if(model){const pages=window.RosganPriceBoard.pages(model);for(let i=0;i<pages.length;i++){const blob=await window.RosganPriceBoard.png(pages[i]);this.downloadCabanaBlob(blob,this.cabanaExportFileBase(this.state.currentCabana)+'-precios-'+(i+1)+'.png');}this.showToast('Placa de precios descargada.');return;}
25727      const blob = await this.createCabanaPriceBoardStoryBlob(allRows);
25728      const remate = this.state.currentCabana || {};
25729      this.downloadCabanaBlob(blob, this.cabanaExportFileBase(remate) + '-placa-precios-story-9x16.png');
25730      this.showToast('Story 9:16 descargada.');
25731    } catch (error) {
25732      this.showToast(error?.message || 'No se pudo generar la Story.');
25733    } finally {
25734      this.setState({ cabanaPriceStoryBusy: false });
25735    }
25736  }
25737  downloadCabanaBlob(blob, fileName) {
25738    const url = URL.createObjectURL(blob);
25739    const link = document.createElement('a');
25740    link.href = url;
25741    link.download = fileName;
25742    document.body.appendChild(link);
25743    link.click();
25744    link.remove();
25745    setTimeout(() => URL.revokeObjectURL(url), 1200);
25746  }
25747  cabanaExportEscape(value) {
25748    return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
25749  }
25750  cabanaExportFileBase(remate = this.state.currentCabana || {}) {
25751    const raw = String(remate?.slug || remate?.nombre_remate || remate?.title || remate?.titulo || 'catalogo-cabanas')
25752      .toLowerCase()
25753      .normalize('NFD')
25754      .replace(/[\u0300-\u036f]/g, '')
25755      .replace(/[^a-z0-9]+/g, '-')
25756      .replace(/^-+|-+$/g, '');
25757    return raw || 'catalogo-cabanas';
25758  }
25759  cabanaCatalogLotCategory(lot) {
25760    return String(this.cabanaLotValue(lot, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], lot?.categoryName || lot?.categoriaNombre || 'Reproductor'));
25761  }
25762  cabanaCatalogMediaUrl(lot) {
25763    const images = this.cabanaLotImages(lot);
25764    const videos = this.cabanaLotVideos(lot);
25765    return window.resolveCabanaMediaUrl(this.cabanaLotSelectedCoverUrl(lot)
25766      || images[0]?.url
25767      || lot?.coverImage?.url
25768      || lot?.coverImage?.downloadUrl
25769      || lot?.cover_image?.url
25770      || this.cabanaLotValue(lot, ['coverImageUrl', 'thumbnailUrl', 'thumbnail', 'imagen', 'imageUrl'], '')
25771      || videos[0]?.thumb
25772      || '');
25773  }
25774  cabanaCatalogExportData(remate = this.state.currentCabana || {}, sourceLots = this.state.cabanaLots || []) {
25775    const lots = this.cabanaSortLotsByCategoryOrder(sourceLots || []);
25776    const participants = this.cabanaRemateParticipants(remate || {});
25777    const cabanaForLot = (lot) => participants.find((cabana) =>
25778      (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
25779      (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) ||
25780      (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) ||
25781      (lot?.cabanaName && cabana.name === lot.cabanaName)
25782    ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
25783    const factLabels = [];
25784    const rows = lots.map((lot, index) => {
25785      const facts = this.cabanaLotOnCardFacts(lot, remate, 8);
25786      facts.forEach((fact) => {
25787        const label = String(fact?.label || '').trim();
25788        if (label && !factLabels.includes(label)) factLabels.push(label);
25789      });
25790      const cabana = cabanaForLot(lot);
25791      const rp = this.cabanaAnalyticsRpId(lot) || this.cabanaLotValue(lot, ['rp', 'RP', 'numero_rp'], '');
25792      const lotNumber = this.cabanaLotNumber(lot);
25793      const url = this.cabanaPublicLotPath(lot);
25794      return {
25795        raw: lot,
25796        categoryDisplayOrder: this.cabanaCategoryDisplayOrder(lot, index + 1),
25797        order: Number(lot?.displayOrder || 0) || index + 1,
25798        itemOrder: Number(lot?.itemDisplayOrder || 0) || index + 1,
25799        lotNumber,
25800        rp,
25801        category: this.cabanaCatalogLotCategory(lot),
25802        cabanaName: lot?.cabanaNombre || lot?.cabanaName || cabana.name || '',
25803        consignataria: remate?.consignataria || remate?.nombre_cuenta || 'Rosgan',
25804        media: this.cabanaCatalogMediaUrl(lot),
25805        facts,
25806        factMap: facts.reduce((acc, fact) => ({ ...acc, [String(fact.label || '').trim()]: fact.value }), {}),
25807        url,
25808      };
25809    });
25810    const groupMap = new Map();
25811    rows.forEach((row) => {
25812      const key = this.consKey(row.category) + '::' + this.consKey(row.lotNumber || row.order);
25813      if (!groupMap.has(key)) groupMap.set(key, { key, category: row.category, categoryDisplayOrder: row.categoryDisplayOrder, lotNumber: row.lotNumber, order: row.order, rows: [] });
25814      groupMap.get(key).rows.push(row);
25815    });
25816    const groups = Array.from(groupMap.values()).map((group) => ({
25817      ...group,
25818      rows: group.rows.sort((a, b) => a.itemOrder - b.itemOrder),
25819    })).sort((a, b) => {
25820      const categoryOrder = a.categoryDisplayOrder - b.categoryDisplayOrder;
25821      if (categoryOrder) return categoryOrder;
25822      return a.order - b.order;
25823    });
25824    return { rows, groups, factLabels, title: remate?.nombre_remate || remate?.title || remate?.titulo || 'Remate de cabanas' };
25825  }
25826  downloadCabanaCatalogExcel() {
25827    const current = this.state.currentCabana || {};
25828    if (!this.canExportCabanaCatalog(current)) {
25829      this.showToast('No tenés permiso para exportar este catálogo.');
25830      return;
25831    }
25832    const data = this.cabanaCatalogExportData(current, this.state.cabanaLots || []);
25833    if (!data.rows.length) { this.showToast('No hay lotes para exportar.'); return; }
25834    const columns = ['Orden', 'Lote', 'RP', 'Categoria', 'Cabana', 'Consignataria', ...data.factLabels, 'Enlace'];
25835    const body = data.rows.map((row) => {
25836      const cells = [
25837        row.order,
25838        row.lotNumber,
25839        row.rp,
25840        row.category,
25841        row.cabanaName,
25842        row.consignataria,
25843        ...data.factLabels.map((label) => row.factMap[label] || ''),
25844        row.url,
25845      ];
25846      return '<tr>' + cells.map((cell) => '<td>' + this.cabanaExportEscape(cell) + '</td>').join('') + '</tr>';
25847    }).join('');
25848    const html = `<!doctype html><html><head><meta charset="utf-8"><style>
25849      body{font-family:Arial,sans-serif;color:#10233b}
25850      h1{font-size:22px;margin:0 0 4px;color:#003b73}
25851      p{margin:0 0 16px;color:#607188}
25852      table{border-collapse:collapse;width:100%}
25853      th{background:#003b73;color:#fff;text-align:left;font-size:12px;letter-spacing:.04em;padding:10px;border:1px solid #003b73}
25854      td{font-size:12px;padding:9px;border:1px solid #d7e4f0;vertical-align:top}
25855      tr:nth-child(even) td{background:#f5f9fd}
25856    </style></head><body>
25857      <h1>${this.cabanaExportEscape(data.title)}</h1>
25858      <p>Catalogo de lotes y RPs ordenado para carga y control.</p>
25859      <table><thead><tr>${columns.map((column) => '<th>' + this.cabanaExportEscape(column) + '</th>').join('')}</tr></thead><tbody>${body}
25859</tbody></table>
25860    </body></html>`;
25861    this.downloadCabanaBlob(new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel;charset=utf-8' }), this.cabanaExportFileBase(current) + '-lotes-rps.xls');
25862  }
25863  downloadCabanaCommercialExcel(rows = []) {
25864    const current = this.state.currentCabana || {};
25865    const reportRows = Array.isArray(rows) ?rows : [];
25866    if (!reportRows.length) { this.showToast('No hay resultados comerciales para exportar.'); return; }
25867    const groupLabels = Array.from(new Set(reportRows.map((row) => row.groupLabel).filter(Boolean)));
25868    const groupLabel = groupLabels.length === 1 ?groupLabels[0] : 'Lote / Corral';
25869    const columns = ['Orden', groupLabel, 'RP / Parte', 'N° / Pintura', 'Categoría', 'Cabaña', 'Consignataria', 'Cantidad', 'Peso prom.', 'Estado', 'Modalidad', 'Precio', 'Total vendido', 'Plazo', 'Comprador'];
25870    const textCell = (value) => '<td class="text">' + this.cabanaExportEscape(value) + '</td>';
25871    const body = reportRows.map((row) => '<tr>' + [
25872      textCell(row.order),
25873      textCell(row.lotNumber),
25874      textCell(row.rp),
25875      textCell(row.paintNumber),
25876      textCell(row.category),
25877      textCell(row.cabana),
25878      textCell(row.consignataria),
25879      '<td class="number">' + this.cabanaExportEscape(row.cantidad) + '</td>',
25880      '<td class="number">' + this.cabanaExportEscape(row.pesoProm) + '</td>',
25881      textCell(row.statusLabel),
25882      textCell(row.priceUnitLabel),
25883      '<td class="currency">' + (row.priceNumeric || '') + '</td>',
25884      '<td class="currency">' + (row.totalNumeric || '') + '</td>',
25885      textCell(row.plazo),
25886      textCell(row.comprador),
25887    ].join('') + '</tr>').join('');
25888    const soldCount = reportRows.filter((row) => row.status === 'vendido').length;
25889    const generatedAt = new Date().toLocaleString('es-AR');
25890    const html = `<!doctype html><html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8"><style>
25891      body{font-family:Arial,sans-serif;color:#10233b}
25892      h1{font-size:22px;margin:0 0 4px;color:#003b73}
25893      p{margin:0 0 14px;color:#607188}
25894      table{border-collapse:collapse;width:100%}
25895      th{background:#003b73;color:#fff;text-align:left;font-size:11px;padding:9px;border:1px solid #003b73}
25896      td{font-size:11px;padding:8px;border:1px solid #d7e4f0;vertical-align:top}
25897      tr:nth-child(even) td{background:#f5f9fd}
25898      .text{mso-number-format:"\\@"}.number{mso-number-format:"0";text-align:right}.currency{mso-number-format:"\\$"#,##0;text-align:right;font-weight:bold}
25899    </style></head><body>
25900      <h1>${this.cabanaExportEscape(current?.nombre_remate || current?.title || current?.titulo || 'Informe comercial')}</h1>
25901      <p>Resultados de remate de cabañas · ${reportRows.length} registros · ${soldCount} vendidos · Exportado ${this.cabanaExportEscape(generatedAt)}</p>
25902      <table><thead><tr>${columns.map((column) => '<th>' + this.cabanaExportEscape(column) + '</th>').join('')}</tr></thead><tbody>${body}</tbody></table>
25903    </body></html>`;
25904    this.downloadCabanaBlob(new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel;charset=utf-8' }), this.cabanaExportFileBase(current) + '-informe-comercial.xls');
25905    this.showToast('Informe comercial exportado.');
25906  }
25907  downloadCabanaCatalogPdf() {
25908    const current = this.state.currentCabana || {};
25909    const data = this.cabanaCatalogExportData(current, this.state.cabanaLots || []);
25910    if (!data.groups.length) { this.showToast('No hay lotes para exportar.'); return; }
25911    const participants = this.cabanaRemateParticipants(current || {});
25912    const mainLogo = participants.find((cabana) => cabana.type === 'principal')?.logoDataUrl || participants[0]?.logoDataUrl || '';
25913    const guests = participants.filter((cabana) => cabana.type !== 'principal' && cabana.logoDataUrl).slice(0, 3);
25914    const consName = current?.consignataria || current?.nombre_cuenta || 'Rosgan';
25915    const consColor = this.consBrandColor(consName);
25916    const consLogo = this.consWhiteLogo(consName) || this.consLogo(consName) || '';
25917    const logoImg = (src, alt, cls = '') => src ?`<img class="${cls}" src="${this.cabanaExportEscape(src)}" alt="${this.cabanaExportEscape(alt)}">` :`<strong class="${cls}">${this.cabanaExportEscape(alt)}</strong>`;
25918    const pages = data.groups.map((group) => {
25919      const first = group.rows[0] || {};
25920      const media = first.media || '';
25921      const rpList = group.rows.map((row) => {
25922        const factItems = (row.facts.length ?row.facts : [
25923          { label: 'RP', value: row.rp },
25924          { label: 'Lote', value: row.lotNumber },
25925        ]).slice(0, 6).map((fact) => `<span><b>${this.cabanaExportEscape(fact.label)}</b><em>${this.cabanaExportEscape(fact.value)}</em></span>`).join('');
25926        return `<article class="rp-card">
25927          <div class="rp-head"><strong>RP ${this.cabanaExportEscape(row.rp)}</strong><small>${this.cabanaExportEscape(row.cabanaName)}</small></div>
25928          <div class="rp-facts">${factItems}</div>
25929        </article>`;
25930      }).join('');
25931      return `<section class="print-page">
25932        <header class="print-head">
25933          <img src="/assets/rosgan-logo-blue.png" alt="Rosgan">
25934          ${logoImg(mainLogo, 'La Cassina', 'main-brand')}
25935        </header>
25936        <div class="print-title">
25937          <small>${this.cabanaExportEscape(data.title)}</small>
25938          <h1>Lote ${this.cabanaExportEscape(group.lotNumber || group.order)}</h1>
25939          <p>${this.cabanaExportEscape(group.category)}</p>
25940        </div>
25941        <figure class="lot-cover">
25942          ${media ?`<img src="${this.cabanaExportEscape(media)}" alt="Lote ${this.cabanaExportEscape(group.lotNumber)}">` :'<div class="cover-empty">Sin imagen cargada</div>'}
25943          <figcaption><strong>Lote ${this.cabanaExportEscape(group.lotNumber || group.order)}</strong><span>${group.rows.map((row) => 'RP ' + row.rp).join(' · ')}</span></figcaption>
25944        </figure>
25945        <div class="rp-grid">${rpList}</div>
25946        <footer class="print-footer">
25947          <div class="cons" style="background:${this.cabanaExportEscape(consColor)}">${consLogo ?`<img src="${this.cabanaExportEscape(consLogo)}" alt="${this.cabanaExportEscape(consName)}">` :''}<span><small>Consignataria</small><strong>${this.cabanaExportEscape(consName)}</strong></span></div>
25948          <span class="site">rosgan.app</span>
25949        </footer>
25950      </section>`;
25951    }).join('');
25952    const html = `<!doctype html><html><head><meta charset="utf-8"><title>${this.cabanaExportEscape(data.title)} - PDF</title><style>
25953      @page{size:A4;margin:12mm}
25954      *{box-sizing:border-box}
25955      body{margin:0;background:#eef5fb;color:#10233b;font-family:Arial,Helvetica,sans-serif}
25956      .print-page{width:210mm;min-height:297mm;margin:0 auto 18px;padding:18mm;background:#fff;page-break-after:always;display:flex;flex-direction:column;gap:15mm}
25957      .print-head{display:flex;align-items:center;justify-content:space-between;gap:18mm}
25958      .print-head img,.print-head .main-brand{max-width:78mm;max-height:22mm;object-fit:contain;color:#203153;font-size:24px}
25959      .print-title small{display:block;color:#0a5ca8;font-weight:900;text-transform:uppercase;letter-spacing:.12em}
25960      .print-title h1{margin:4mm 0 1mm;font-size:40px;line-height:1;color:#002e5f}
25961      .print-title p{margin:0;color:#607188;font-size:16px;font-weight:800;text-transform:uppercase}
25962      .lot-cover{position:relative;margin:0;border-radius:16px;overflow:hidden;min-height:92mm;background:#e8eff6}
25963      .lot-cover>img{display:block;width:100%;height:92mm;object-fit:cover}
25964      .cover-empty{height:92mm;display:grid;place-items:center;color:#607188;font-weight:800}
25965      .lot-cover:after{content:"";position:absolute;inset:50% 0 0;background:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,29,61,.78))}
25966      .lot-cover figcaption{position:absolute;left:10mm;right:10mm;bottom:9mm;z-index:1;color:#fff}
25967      .lot-cover figcaption strong{display:block;font-size:34px;line-height:1}
25968      .lot-cover figcaption span{display:block;margin-top:2mm;font-size:15px;font-weight:800}
25969      .rp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7mm}
25970      .rp-card{border:1px solid #d7e4f0;border-radius:14px;padding:7mm;background:#f8fbfe}
25971      .rp-head{display:flex;align-items:flex-start;justify-content:space-between;gap:6mm;border-bottom:1px solid #dce7f2;padding-bottom:4mm;margin-bottom:4mm}
25972      .rp-head strong{font-size:20px;color:#002e5f}.rp-head small{color:#607188;font-weight:800;text-align:right}
25973      .rp-facts{display:grid;gap:3mm}.rp-facts span{display:flex;justify-content:space-between;gap:8mm;border-bottom:1px solid #e7eef6;padding-bottom:2mm}
25974      .rp-facts b{color:#7b8aa0;text-transform:uppercase;font-size:11px}.rp-facts em{font-style:normal;color:#10233b;font-weight:900;text-align:right}
25975      .print-footer{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:10mm}
25976      .cons{display:flex;align-items:center;gap:6mm;border-radius:14px;padding:5mm 7mm;color:#fff;min-width:95mm}
25977      .cons img{width:18mm;height:18mm;object-fit:contain}.cons small{display:block;opacity:.75;font-weight:900;text-transform:uppercase;letter-spacing:.08em}.cons strong{font-size:18px}
25978      .site{font-size:18px;font-weight:900;color:#002e5f}
25979      @media print{body{background:#fff}.print-page{margin:0;box-shadow:none}}
25980    </style></head><body>${pages}</body></html>`;
25981    const popup = window.open('', '_blank');
25982    if (!popup) {
25983      this.downloadCabanaBlob(new Blob([html], { type: 'text/html;charset=utf-8' }), this.cabanaExportFileBase(current) + '-catalogo-pdf.html');
25984      this.showToast('Se descargo el catalogo HTML para imprimir como PDF.');
25985      return;
25986    }
25987    popup.document.open();
25988    popup.document.write(html);
25989    popup.document.close();
25990    popup.setTimeout(() => { try { popup.focus(); popup.print(); } catch {} }, 700);
25991  }
25992  async shareCabanaLotStory(lot = null, context = {}) {
25993    const target = lot || this.state.currentCabanaLot;
25994    if (!target || this.state.cabanaLotStoryBusy) return;
25995    const current = context?.remate || this.state.currentCabana || {};
25996    const shareContext = { remate: current, contact: context?.contact || this.cabanaContactData(current) };
25997    this.setState({ cabanaLotStoryBusy: true });
25998    const { lotNumber, rp, title, auctionTitle } = this.cabanaLotShareTitle(target);
25999    const url = this.cabanaPublicShareUrl(target);
26000    const auctionId = this.cabanaRemateId(this.state.currentCabana) || String(target?.remateId || target?.auctionId || 'remate');
26001    const fileBase = ['rosgan', 'remate-' + auctionId, 'lote-' + (lotNumber || 'rp'), rp || 'rp'].join('-').toLowerCase().replace(/[^a-z0-9-]+/g, '-');
26002    let completed = false;
26003    try {
26004      const blob = await this.createCabanaLotStoryBlob(target, shareContext);
26005      const file = new File([blob], fileBase + '.png', { type: 'image/png' });
26006      const text = title + ' - ' + auctionTitle + ' ' + url;
26007      if (navigator.share && (!navigator.canShare || navigator.canShare({ files: [file] }))) {
26008        await navigator.share({ title, text, files: [file] });
26009        completed = true;
26010      } else {
26011        this.downloadCabanaBlob(blob, file.name);
26012        if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
26013        this.showToast('Imagen para historias descargada y enlace copiado');
26014        completed = true;
26015      }
26016    } catch (error) {
26017      if (error?.name !== 'AbortError') this.showToast('No se pudo generar la imagen para historias.');
26018    } finally {
26019      this.setState({ cabanaLotStoryBusy: false });
26020    }
26021    if (!completed) return;
26022    this.trackCabanaAnalytics('story_card_generated', target, {
26023      metadata: { action: 'story_card_generated', url, format: 'story_9_16' },
26024    });
26025  }
26026  cabanaAnalyticsPayload(eventType, lot = null, extra = {}) {
26027    const current = this.state.currentCabana || {};
26028    const values = lot?.values || {};
26029    const auctionId = String(lot?.remateId || this.cabanaRemateId(current) || '').trim();
26030    const participants = current ?this.cabanaRemateParticipants(current) : [];
26031    const selectedCabana = participants.find((cabana) =>
26032      (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
26033      (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) ||
26034      (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) ||
26035      (lot?.cabanaName && cabana.name === lot.cabanaName)
26036    ) || {};
26037    const lotId = lot ?this.cabanaLotId(lot) : '';
26038    const lotIdentity = lot ?window.CabanaLotIdentity.resolve(lot, current) : null;
26039    const rp = String(lot?.rp || values.rp || lot?.identifier || '').trim();
26040    const clientId = this.cabanaAnalyticsClientId();
26041    return {
26042      auctionId,
26043      auctionTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña',
26044      eventType,
26045      lotId,
26046      lotCert: lotId,
26047      lotTitle: lot?.categoryName || lot?.categoriaNombre || lot?.name || '',
26048      lotLabel: lotIdentity ?[lotIdentity.label, lotIdentity.number].filter(Boolean).join(' ') : '',
26049      categoryId: lot?.categoryId || '',
26050      categoryName: lot?.categoryName || lot?.categoriaNombre || values.categoriaNombre || '',
26051      cabanaId: lot?.cabanaId || selectedCabana.id || '',
26052      cabanaName: lot?.cabanaNombre || lot?.cabanaName || selectedCabana.name || '',
26053      rpId: rp,
26054      rp,
26055      sessionId: clientId,
26056      clientId,
26057      source: this.state.publicAdvanceMode === 'lacassina' ?'lacassina-public' : (this.state.publicAdvanceMode === 'cabanas' ?'cabanas-public' : 'cabanas-app'),
26058      metadata: {
26059        route: window.location.pathname,
26060        ...(extra.metadata || {}),
26061      },
26062      ...extra,
26063    };
26064  }
26065  async trackCabanaAnalytics(eventType, lot = null, extra = {}) {
26066    try {
26067      const payload = this.cabanaAnalyticsPayload(eventType, lot, extra);
26068      if (!payload.auctionId || !String(eventType || '').trim()) return;
26069      const body = JSON.stringify(payload);
26070      const endpoint = '/api/cabanas-analytics/events';
26071      let token = '';
26072      if (this.state.user && !this.state.user.isAnonymous) { token = await this.authToken(false); if (!token) return; }
26073      if (this.cabanaCatalogLocked()) return;
26074      if (!token && navigator.sendBeacon) {
26075        const queued = navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }));
26076        if (queued) return;
26077      }
26078      fetch(endpoint, {
26079        method: 'POST',
26080        headers: { 'Content-Type': 'application/json', ...(token ?{ Authorization: 'Bearer ' + token } : {}) },
26081        body,
26082        keepalive: true,
26083      }).catch(() => {});
26084    } catch {}
26085  }
26086  remateDate(item) { if (!item) return null; const raw = item.fecha_remate || item.fecha || item.fecha_hora_remate || item.fechaHoraRemate || item.fechaHoraInicio || item.startsAt || (item?.schedule?.fecha ?item.schedule.fecha + 'T' + (item.schedule.hora || '00:00:00') : ''); const d = raw ?new Date(raw) : null; return d && !isNaN(d) ?d : null; }
26087  feriaDate(f) {
26088    const raw = f?.fecha || f?.date || f?.startsAt;
26089    if (!raw) return null;
26090    const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(raw));
26091    const d = day ? new Date(raw + 'T' + (f.hora || '12:00')) : new Date(raw);
26092    return !isNaN(d) ? d : null;
26093  }
26094  feriaLabel(f) { const d = this.feriaDate(f); if (!d) return 'Fecha a confirmar'; return d.toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) + ' · ' + d.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }) + ' h'; }
26095  ytThumb(url) { const id = (url || '').match(/(?:v=|youtu\.be\/|\/vi\/|embed\/)([\w-]{11})/)?.[1]; return id ?'https://img.youtube.com/vi/' + id + '/hqdefault.jpg' : 'assets/feria.png'; }
26096  currentLot() { return (this.state.lots || []).find((l) => l.cert === this.state.selectedCert) || null; }
26097  // ---- Ofertas / bids API ----
26098  get API() { return ''; }
26099  async authToken(forceRefresh = false) {
26100    const epoch = this._sessionEpoch || 0;
26101    const firebase = await this.ensureFirebase();
26102    const auth = firebase.auth();
26103    if (this._signingOut || epoch !== (this._sessionEpoch || 0)) throw new Error('La sesión cambió. Volvé a intentar.');
26104    if (!auth.currentUser) {
26105      await new Promise((resolve, reject) => {
26106        let unsubscribe = () => {};
26107        const timer = setTimeout(() => { unsubscribe(); reject(new Error('No pudimos restaurar la sesión. Revisá la conexión.')); }, 12000);
26108        unsubscribe = auth.onAuthStateChanged(() => { clearTimeout(timer); unsubscribe(); resolve(); }, (error) => { clearTimeout(timer); unsubscribe(); reject(error); });
26109      });
26110    }
26111    if (this._signingOut || epoch !== (this._sessionEpoch || 0)) throw new Error('La sesión cambió. Volvé a intentar.');
26112    const user = auth.currentUser;
26113    if (!user) {
26114      if (this.state.user) throw new Error('La sesión ya no está disponible. Volvé a ingresar.');
26115      return '';
26116    }
26117    // A network/token error must not silently turn an authenticated call into a guest request.
26118    const token = await user.getIdToken(forceRefresh);
26119    if (this._signingOut || epoch !== (this._sessionEpoch || 0) || auth.currentUser?.uid !== user.uid) throw new Error('La sesión cambió. Volvé a intentar.');
26120    return token;
26121  }
26122  async cabanaApiFetch(path, opts = {}) {
26123    path = String(path || '').replace(/\s+\|\|\s+/, '?');
26124    const request = async (forceRefresh) => {
26125      const method = String(opts.method || 'GET').toUpperCase();
26126      const token = await this.authToken(forceRefresh);
26127      if (!token && method !== 'GET') throw new Error('Inicia sesion para guardar el remate.');
26128      const headers = { Accept: 'application/json', ...(opts.body ?{ 'Content-Type': 'application/json' } : {}), ...(opts.headers || {}), ...(token ?{ Authorization: 'Bearer ' + token } : {}) };
26129      const response = await fetch(path, { ...opts, headers });
26130      const text = await response.text();
26131      let data = null;
26132      try { data = text ?JSON.parse(text.replace(/^\uFEFF/, '')) : null; } catch { data = text || null; }
26133      return { response, data };
26134    };
26135    let result = await request(false);
26136    if (result.response.status === 401) result = await request(true);
26137    if (!result.response.ok) {
26138      const validation = result.data?.errors && typeof result.data.errors === 'object' ?Object.values(result.data.errors).flat().filter(Boolean)[0] : '';
26139      const error = new Error(result.data?.detail || validation || result.data?.message || result.data?.title || ('HTTP ' + result.response.status));
26140      error.status = result.response.status;
26141      error.data = result.data;
26142      throw error;
26143    }
26144    return result.data;
26145  }
26146  async apiFetch(path, opts) {
26147    window.RosganDevActivity?.start(this);
26148    path = String(path || '')
26149      .replace(/\s+\|\|\s+/, '?')
26150      .replace(/^\/api\/Lots/i, '/api/lots')
26151      .replace(/^\/api\/Bids/i, '/api/bids')
26152      .replace(/^\/api\/Establishments/i, '/api/establishments')
26153      .replace(/^\/api\/BuyerAuthorizations/i, '/api/buyerauthorizations')
26154      .replace(/^\/api\/Users/i, '/api/users')
26155      .replace(/^\/api\/Values/i, '/api/values')
26156      .replace(/^\/api\/Catalogs/i, '/api/catalogs');
26157    const request = async (forceRefresh) => {
26158      const token = await this.authToken(forceRefresh);
26159      const isFormData = typeof FormData !== 'undefined' && opts?.body instanceof FormData;
26160      const headers = { Accept: 'application/json', ...(!isFormData ?{ 'Content-Type': 'application/json' } : {}), ...(opts?.headers || {}), ...(token ?{ Authorization: 'Bearer ' + token } : {}) };
26161      const response = await fetch(this.API + path, { ...opts, headers });
26162      const txt = await response.text();
26163      let data = null;
26164      try { data = txt ?JSON.parse(txt) : null; } catch { data = txt || null; }
26165      return { response, data };
26166    };
26167    let result = await request(false);
26168    if (result.response.status === 401) result = await request(true);
26169    if (!result.response.ok) {
26170      const validationErrors = result.data?.errors && typeof result.data.errors === 'object' ?Object.values(result.data.errors).flat().filter((value) => typeof value === 'string').join(' · ') : '';
26171      const plainTextDetail = typeof result.data === 'string' && !/<(?:!doctype|html|head|body)\b/i.test(result.data)
26172        ?result.data.trim()
26173        : '';
26174      const detail = plainTextDetail || result.data?.message || result.data?.detail || (typeof result.data?.error === 'string' ?result.data.error : '') || validationErrors || result.data?.title;
26175      const statusMessage = result.response.status === 401
26176        ?'No pudimos autenticar la solicitud. Actualizá la página e intentá nuevamente.'
26177        : result.response.status === 429 ? 'El servicio está recibiendo demasiadas consultas. Esperá un momento antes de volver a intentar.' : ('HTTP ' + result.response.status);
26178      const error = new Error(detail || statusMessage);
26179      error.status = result.response.status;
26180      const retryAfter = result.response.headers.get('Retry-After');
26181      const retryDelay = retryAfter && /^\d+(?:\.\d+)?$/.test(retryAfter.trim()) ? Number(retryAfter) * 1000 : Date.parse(retryAfter || '') - Date.now();
26182      error.retryAfterMs = Number.isFinite(retryDelay) ? Math.max(0,retryDelay) : 0;
26183      error.data = result.data;
26184      throw error;
26185    }
26186    return result.data;
26187  }
26188  normBid(b, cert) {
26189    if (b == null) return null;
26190    if (typeof b === 'number') return { id: '', amount: b, status: 'active', bidder: '', date: '' };
26191    const amount = Number(b.amount || b.price  || b.pricePerKg || b.value  || b.monto || 0) || 0;
26192    const status = String(b.status || b.state || b.estado  || 'pending').toLowerCase();
26193    return {
26194      id: String(b.id || b.bidId || b._id  || b.bidCert || ''),
26195      cert: b.lotCert || b.cert  || cert || '',
26196      amount,
26197      status,
26198      bidder: b.bidderName || b.buyerName || b.buyerDisplayName || b.buyer?.name || b.buyer?.displayName || b.userName || b.comprador || '',
26199      buyerUid: String(b.buyerUid || b.userUid || b.bidderUid  || b.createdByUid || ''),
26200      buyerEmail: String(b.buyerEmail || b.userEmail  || b.bidderEmail || '').toLowerCase(),
26201      lotName: b.lotName || b.categoria  || b.category || '',
26202      date: b.createdAt || b.date  || b.fecha || '',
26203      isWinning: b.isWinning || b.winning  || b.isLeading || b.leading  || b.isHighest || null,
26204      topAmount: Number(b.highestBid || b.topBid  || b.currentHighest || b.leadingAmount  || b.maxBid || 0) || 0,
26205    };
26206  }
26207  ownBidsOnly(bids) {
26208    const uid = String(this.state.user?.uid || '');
26209    const email = String(this.state.user?.email || '').toLowerCase();
26210    return (bids || []).filter((bid) => {
26211      if (bid.buyerUid) return !!uid && bid.buyerUid === uid;
26212      if (bid.buyerEmail) return !!email && bid.buyerEmail === email;
26213      return true;
26214    });
26215  }
26216  computeStanding(cert, myAmount, isWinning, topAmount) {
26217    const liveTop = Math.max(0, ...(this.state.lotBids[cert] || []).map((b) => Number(b.amount) || 0));
26218    const best = Math.max(Number(topAmount) || 0, liveTop, Number(myAmount) || 0);
26219    if (!best || !myAmount) return null;
26220    const winning = typeof isWinning === 'boolean' ?isWinning : Number(myAmount) >= best;
26221    return winning
26222      ?{ key: 'winning', label: 'Vas ganando', bg: 'rgba(31,122,99,0.14)', color: '#1f7a63', myAmount: Number(myAmount), topAmount: best }
26223      : { key: 'outbid', label: 'Te superaron', bg: 'rgba(230,57,70,0.14)', color: '#c22', myAmount: Number(myAmount), topAmount: best };
26224  }
26225  openRepublish(lot) {
26226    const plazoId = lot.plazoEsOtro ?'otro' : String(lot.plazoId || '');
26227    this.setState({
26228      republishLot: lot,
26229      republishForm: {
26230        fechaSubasta: '',
26231        listoParaCargar: lot.raw?.listoParaCargar === true,
26232        fechaDisponibleCarga: lot.fechaDisponibleCarga || '',
26233        precioSugerido: String(lot.baseNum || ''),
26234        plazoId,
26235        plazoOtro: plazoId === 'otro' ?String(lot.plazoOtro || '') : '',
26236      },
26237      republishMessage: null,
26238    });
26239  }
26240  closeRepublish() { if (!this.state.republishSaving) this.setState({ republishLot: null, republishForm: null, republishMessage: null }); }
26241  setRepublish(key, value) { this.setState({ republishForm: { ...(this.state.republishForm || {}), [key]: value, ...(key === 'listoParaCargar' && value === true ?{ fechaDisponibleCarga: '' } : {}) }, republishMessage: null }); }
26242  async submitRepublish() {
26243    if (this.state.republishSaving) return;
26244    const form = this.state.republishForm;
26245    const lot = this.state.republishLot;
26246    if (!form || !lot) return;
26247    const fail = (text) => this.setState({ republishMessage: { text, ok: false } });
26248    const isIsoDate = (value) => {
26249      const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim());
26250      if (!match) return false;
26251      const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
26252      return date.getFullYear() === Number(match[1]) && date.getMonth() === Number(match[2]) - 1 && date.getDate() === Number(match[3]);
26253    };
26254    const fechaSubasta = String(form.fechaSubasta || '').trim();
26255    const fechaDisponibleCarga = String(form.fechaDisponibleCarga || '').trim();
26256    const precioSugerido = this.numOrNull(form.precioSugerido);
26257    if (!isIsoDate(fechaSubasta)) return fail('Elegí una fecha de subasta válida.');
26258    const availabilityError = this.loadAvailabilityError(fechaSubasta, form.listoParaCargar, fechaDisponibleCarga);
26259    if (availabilityError) return fail(availabilityError);
26260    if (!(precioSugerido > 0)) return fail('El precio sugerido debe ser mayor a $0.');
26261    if (!form.plazoId) return fail('Elegí el plazo de pago.');
26262    if (form.plazoId === 'otro' && !String(form.plazoOtro).trim()) return fail('Especificá el plazo.');
26263    const body = {
26264      fechaSubasta,
26265      listoParaCargar: form.listoParaCargar === true,
26266      fechaDisponibleCarga: form.listoParaCargar === true ?null : fechaDisponibleCarga,
26267      precioSugerido,
26268      plazoId: String(form.plazoId),
26269      plazoOtro: form.plazoId === 'otro' ?String(form.plazoOtro).trim() : null,
26270    };
26271    this.setState({ republishSaving: true, republishMessage: { text: 'Republicando lote…', ok: true } });
26272    try {
26273      await this.apiFetch('/api/Lots/republish/' + encodeURIComponent(lot.cert), { method: 'POST', body: JSON.stringify(body) });
26274      await Promise.all([this.fetchLots(), this.fetchMyLots()]);
26275      this.setState({ republishSaving: false, republishLot: null, republishForm: null, republishMessage: null });
26276      this.showToast('Lote republicado para el ' + fechaSubasta + '.');
26277    } catch (error) {
26278      this.setState({ republishSaving: false, republishMessage: { text: error?.message || 'No se pudo republicar el lote.', ok: false } });
26279    }
26280  }
26281  bidStatusMeta(st) {
26282    const s = String(st || '').toLowerCase();
26283    if (/(approv|aprob|won|adjud|accept)/.test(s)) return { label: 'Aprobada', bg: 'rgba(31,122,99,0.14)', color: '#1f7a63' };
26284    if (/(outbid|superad)/.test(s)) return { label: 'Superada', bg: 'rgba(230,57,70,0.14)', color: '#c22' };
26285    if (/(reject|rechaz|declin|lost)/.test(s)) return { label: 'Rechazada', bg: 'rgba(230,57,70,0.14)', color: '#c22' };
26286    if (/(pend|espera)/.test(s)) return { label: 'Pendiente', bg: 'rgba(230,158,20,0.16)', color: '#a8730a' };
26287    return { label: 'Activa', bg: 'rgba(10,92,168,0.14)', color: '#0a5ca8' };
26288  }
26289  async fetchLotBids(cert) {
26290    if (!cert) return;
26291    this.setState({ lotBidsLoading: { ...this.state.lotBidsLoading, [cert]: true } });
26292    try {
26293      const data = await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert));
26294      const arr = this.listFrom(data, ['items', 'bids', 'data']);
26295      const bids = arr.map((b) => this.normBid(b, cert)).filter(Boolean).sort((a, b) => b.amount - a.amount);
26296      this.setState({ lotBids: { ...this.state.lotBids, [cert]: bids }, lotBidsLoading: { ...this.state.lotBidsLoading, [cert]: false } });
26297    } catch { this.setState({ lotBidsLoading: { ...this.state.lotBidsLoading, [cert]: false } }); }
26298  }
26299  async fetchMyBids() {
26300    this.setState({ myBidsLoading: true, myBidsError: '' });
26301    try {
26302      const data = await this.apiFetch('/api/Bids/mine?limit=50');
26303      const arr = this.listFrom(data, ['items', 'bids', 'data']);
26304      const list = this.ownBidsOnly(arr.map((b) => this.normBid(b)).filter(Boolean));
26305      this.setProfileFetchInfo('ofertas', list.length, '');
26306      this.setState({ myBids: list, myBidsLoading: false, myBidsError: '' });
26307    } catch (e) {
26308      if (e?.status === 403) {
26309        this.setProfileFetchInfo('ofertas', 0, '');
26310        this.setState({ myBids: [], myBidsLoading: false, myBidsError: '' });
26311        return;
26312      }
26313      this.setProfileFetchInfo('ofertas', null, e?.message || 'Error');
26314      this.setState({ myBids: [], myBidsLoading: false, myBidsError: e?.message || 'No se pudieron cargar tus ofertas.' });
26315    }
26316  }
26317  async fetchRoslotDailyReport(force = false) {
26318    if (this.state.roslotDailyReportLoading && !force) return;
26319    const date = this.state.roslotDailyReportDate || this.localIsoDate();
26320    this.setState({
26321      roslotDailyReportLoading: true,
26322      roslotDailyReportError: '',
26323      roslotDailyReportDate: date,
26324    });
26325    try {
26326      const data = await this.apiFetch('/api/roslot/daily-summary?date=' + encodeURIComponent(date));
26327      this.setState({
26328        roslotDailyReport: data && typeof data === 'object' ?data : null,
26329        roslotDailyReportLoading: false,
26330        roslotDailyReportError: '',
26331      });
26332    } catch (error) {
26333      this.setState({
26334        roslotDailyReport: null,
26335        roslotDailyReportLoading: false,
26336        roslotDailyReportError: error?.message || 'No se pudo cargar el cierre comercial del día.',
26337      });
26338    }
26339  }
26340  setRoslotDailyReportDate(event) {
26341    const date = String(event?.target?.value || '').trim();
26342    if (!date) return;
26343    this.setState({ roslotDailyReportDate: date }, () => this.fetchRoslotDailyReport(true));
26344  }
26345  setRoslotDailyReportFilter(filter) {
26346    this.setState({ roslotDailyReportFilter: ['buyer', 'seller'].includes(filter) ?filter : 'all' });
26347  }
26348  setRoslotDailyReportSearch(event) {
26349    this.setState({ roslotDailyReportSearch: String(event?.target?.value || '') });
26350  }
26351  roslotDailyReportView() {
26352    const s = this.state;
26353    const report = s.roslotDailyReport && typeof s.roslotDailyReport === 'object' ?s.roslotDailyReport : null;
26354    const stats = report?.stats || {};
26355    const selectedDate = s.roslotDailyReportDate || report?.date || this.localIsoDate();
26356    const parsedDate = /^\d{4}-\d{2}-\d{2}$/.test(selectedDate) ?new Date(selectedDate + 'T12:00:00') : null;
26357    const dateLabel = parsedDate && !Number.isNaN(parsedDate.getTime())
26358      ?parsedDate.toLocaleDateString('es-AR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })
26359      : selectedDate;
26360    const items = Array.isArray(report?.items) ?report.items : [];
26361    const query = String(s.roslotDailyReportSearch || '').trim().toLocaleLowerCase('es');
26362    const filter = ['buyer', 'seller'].includes(s.roslotDailyReportFilter) ?s.roslotDailyReportFilter : 'all';
26363    const initials = (name) =>
26363 String(name || '?').split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part.charAt(0)).join('').toUpperCase() || '?';
26364    const rows = items.filter((item) => {
26365      if (report?.scope !== 'all' && filter !== 'all' && ![filter, 'both'].includes(item.viewerRole)) return false;
26366      if (!query) return true;
26367      return [item.cert, item.category, item.establishment, item.location, item.buyer?.name, item.buyer?.email, item.seller?.name, item.seller?.email]
26368        .some((value) => String(value || '').toLocaleLowerCase('es').includes(query));
26369    }).map((item) => {
26370      const amount = Number(item.amount) || 0;
26371      const total = Number(item.estimatedTotal);
26372      const closedAt = item.closedAt ?new Date(item.closedAt) : null;
26373      const priceUnit = item.priceBasis === 'kg' ?'/ kg vivo' : (item.priceBasis === 'meat' ?'/ kg carne' : (item.priceBasis === 'head' ?'/ cabeza' : '/ unidad'));
26374      const yieldPercent = Number(item.yieldPercent) || 0;
26375      const contact = (person, type) => {
26376        const phone = String(person?.phone || '').trim();
26377        const email = String(person?.email || '').trim();
26378        return {
26379          type,
26380          name: person?.name || 'Sin nombre informado',
26381          initials: initials(person?.name),
26382          phone: phone || 'Sin teléfono informado',
26383          email: email || 'Sin email informado',
26384          hasPhone: !!phone,
26385          hasNoPhone: !phone,
26386          hasEmail: !!email,
26387          hasNoEmail: !email,
26388          phoneHref: phone ?'tel:' + phone.replace(/[^+\d]/g, '') : '',
26389          emailHref: email ?'mailto:' + email : '',
26390        };
26391      };
26392      return {
26393        cert: item.cert || '—',
26394        category: item.category || 'Hacienda',
26395        thumbnail: item.thumbnail || '',
26396        hasThumbnail: !!item.thumbnail,
26397        hasNoThumbnail: !item.thumbnail,
26398        heads: (Number(item.heads) || 0).toLocaleString('es-AR'),
26399        weight: Number(item.averageWeight) > 0
26400          ?Math.round(Number(item.averageWeight)).toLocaleString('es-AR') + ' kg prom.' + (item.priceBasis === 'meat' && yieldPercent > 0 ?' · ' + yieldPercent.toLocaleString('es-AR') + '% rinde' : '')
26401          : 'Peso a confirmar',
26402        price: amount > 0 ?this.fmtARS(amount) : 'A confirmar',
26403        priceUnit,
26404        total: Number.isFinite(total) && total > 0 ?this.fmtARS(total) : 'A confirmar',
26405        condition: item.condition || 'Condición no informada',
26406        location: item.location || 'Ubicación no informada',
26407        establishment: item.establishment || 'Establecimiento no informado',
26408        closedTime: closedAt && !Number.isNaN(closedAt.getTime())
26409          ?closedAt.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' h'
26410          : 'Horario no informado',
26411        buyer: contact(item.buyer, 'Comprador'),
26412        seller: contact(item.seller, 'Vendedor'),
26413        open: () => this.openLot(item.cert),
26414      };
26415    });
26416    const estimatedTotal = Number(stats.estimatedTotal) || 0;
26417    const transactions = Number(stats.transactions) || 0;
26418    const heads = Number(stats.heads) || 0;
26419    const buyers = Number(stats.buyers) || 0;
26420    const sellers = Number(stats.sellers) || 0;
26421    return {
26422      goRoslotSummary: () => this.go('roslotSummary'),
26423      goRoslotPool: () => this.go('pool'),
26424      refreshRoslotReport: () => this.fetchRoslotDailyReport(true),
26425      onRoslotReportDate: (event) => this.setRoslotDailyReportDate(event),
26426      onRoslotReportSearch: (event) => this.setRoslotDailyReportSearch(event),
26427      roslotReportLoading: !!s.roslotDailyReportLoading,
26428      roslotReportReady: !!report && !s.roslotDailyReportLoading && !s.roslotDailyReportError,
26429      roslotReportHasError: !!s.roslotDailyReportError && !s.roslotDailyReportLoading,
26430      roslotReportError: s.roslotDailyReportError || '',
26431      roslotReportDate: selectedDate,
26432      roslotReportDateLabel: dateLabel ?dateLabel.charAt(0).toUpperCase() + dateLabel.slice(1) : '',
26433      roslotReportScopeLabel: report?.scope === 'all' ?'Vista general del mercado' : 'Tus operaciones del día',
26434      roslotReportHeadline: estimatedTotal > 0 ?this.fmtARS(estimatedTotal) : 'Sin monto estimado',
26435      roslotReportHeadlineDetail: estimatedTotal > 0 ?'Volumen bruto estimado adjudicado' : 'Los lotes sin base por kg o cabeza no se proyectan',
26436      roslotReportRows: rows,
26437      roslotReportHasRows: rows.length > 0,
26438      roslotReportEmpty: rows.length === 0,
26439      roslotReportSearch: s.roslotDailyReportSearch || '',
26440      roslotReportShowRoleFilters: report?.scope !== 'all',
26441      roslotReportKpis: [
26442        { icon: 'dc-lucide-banknote', label: 'Volumen estimado', value: estimatedTotal > 0 ?this.fmtARS(estimatedTotal) : '$0', detail: 'Importe bruto comercial' },
26443        { icon: 'dc-lucide-beef', label: 'Cabezas vendidas', value: heads.toLocaleString('es-AR'), detail: transactions === 1 ?'1 lote adjudicado' : transactions.toLocaleString('es-AR') + ' lotes adjudicados' },
26444        { icon: 'dc-lucide-users', label: 'Compradores', value: buyers.toLocaleString('es-AR'), detail: 'Participantes adjudicados' },
26445        { icon: 'dc-lucide-handshake', label: 'Vendedores', value: sellers.toLocaleString('es-AR'), detail: 'Contrapartes vendedoras' },
26446      ],
26447      roslotReportFilters: [
26448        { label: 'Todas', active: filter === 'all', cls: filter === 'all' ?' is-active' : '', go: () => this.setRoslotDailyReportFilter('all') },
26449        { label: 'Mis compras', active: filter === 'buyer', cls: filter === 'buyer' ?' is-active' : '', go: () => this.setRoslotDailyReportFilter('buyer') },
26450        { label: 'Mis ventas', active: filter === 'seller', cls: filter === 'seller' ?' is-active' : '', go: () => this.setRoslotDailyReportFilter('seller') },
26451      ],
26452    };
26453  }
26454  async fetchPendingApprovals() {
26455    this.setState({ pendingBidsLoading: true, pendingBidsError: '' });
26456    try {
26457      const data = await this.apiFetch('/api/Bids/pending-approvals?limit=50');
26458      const arr = this.listFrom(data, ['items', 'bids', 'data']);
26459      const list = arr.map((b) =>
26459 this.normBid(b)).filter(Boolean);
26460      this.setProfileFetchInfo('pendientes', list.length, '');
26461      this.setState({ pendingBids: list, pendingBidsLoading: false, pendingBidsError: '' });
26462    } catch (e) { this.setProfileFetchInfo('pendientes', null, e?.message || 'Error'); this.setState({ pendingBids: [], pendingBidsLoading: false, pendingBidsError: e?.message || 'No se pudieron cargar ofertas pendientes.' }); }
26463  }
26464  async actOnBid(cert, bidId, action) {
26465    if (!cert || !bidId || this.state.bidActioning) return;
26466    this.setState({ bidActioning: bidId });
26467    try {
26468      await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert) + '/' + encodeURIComponent(bidId) + '/' + action, { method: 'POST' });
26469      this.showToast(action === 'approve' ?'Oferta aprobada.' : 'Oferta rechazada.');
26470      this.setState({ bidActioning: '' });
26471      this.fetchPendingApprovals();
26472      this.fetchLotBids(cert);
26473    } catch {
26474      this.setState({ bidActioning: '' });
26475      this.showToast('No se pudo procesar la oferta.');
26476    }
26477  }
26478  currentBid(lot) {
26479    const placed = this.state.bidsPlaced[lot.cert] || 0;
26480    const live = (this.state.lotBids[lot.cert] || []).map((b) => b.amount);
26481    return Math.max(lot.baseNum, lot.currentBidAmount || 0, lot.soldAmount || 0, placed, ...(lot.bids || []), ...live, 0);
26482  }
26483  bidAdj(cert) { const a = this.state.bidByCert[cert]; return a || { increment: this.INCREMENTS[0], offset: this.INCREMENTS[0] }; }
26484  setBidAdj(cert, adj) { this.setState({ bidByCert: { ...this.state.bidByCert, [cert]: adj } }); }
26485  showRoslotBidSuccess(cert, amount) {
26486    if (this._roslotBidSuccessTimer) clearTimeout(this._roslotBidSuccessTimer);
26487    this.setState({ roslotBidSuccessFx: { cert: String(cert || ''), amount: Number(amount) || 0 } });
26488    this._roslotBidSuccessTimer = setTimeout(() => {
26489      this._roslotBidSuccessTimer = null;
26490      this.setState({ roslotBidSuccessFx: null });
26491    }, 1750);
26492  }
26493  async quickBid(cert, marketAmount, quickOffset = null) {
26494    if (!cert || this.state.quickBidCert) return;
26495    const lot = (this.state.lots || []).find((item) => item.cert === cert);
26496    if (!lot || lot.auctionStatus !== 'open' || lot.isClosed) {
26497      this.showToast('La subasta de este lote ya está cerrada. No se pueden realizar más ofertas.');
26498      return;
26499    }
26500    if (lot?.sellerUid && this.state.user?.uid && lot.sellerUid === this.state.user.uid) {
26501      this.showToast('No podés ofertar sobre un lote publicado por tu usuario.');
26502      return;
26503    }
26504    if (lot?.currentBidBuyerUid && this.state.user?.uid && lot.currentBidBuyerUid === this.state.user.uid) {
26505      this.showToast('¡Vas ganando! No necesitás volver a ofertar.');
26506      return;
26507    }
26508    const token = await this.authToken();
26509    if (!token) { this.showToast('Iniciá sesión para enviar una oferta.'); return; }
26510    const adjustment = this.bidAdj(cert);
26511    const myAmount = Math.max(0, ...(this.state.myBids || []).filter((bid) => bid.cert === cert).map((bid) => Number(bid.amount) || 0));
26512    const liveTop = Math.max(0, ...(this.state.lotBids[cert] || []).map((bid) => Number(bid.amount) || 0));
26513    const offset = Number.isFinite(Number(quickOffset)) ?Number(quickOffset) : adjustment.offset;
26514    const target = Math.max(Number(marketAmount) || 0, myAmount, liveTop) + offset;
26515    this.setState({ quickBidCert: cert });
26516    try {
26517      await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert), { method: 'POST', body: JSON.stringify({ amount: target }) });
26518      this.showRoslotBidSuccess(cert, target);
26519      this.setState({ bidsPlaced: { ...this.state.bidsPlaced, [cert]: target }, quickBidCert: '' });
26520      await Promise.all([this.fetchMyBids(), this.fetchLotBids(cert), this.syncAuctionStatuses()]);
26521    } catch (error) {
26522      this.setState({ quickBidCert: '' });
26523      this.showToast(this.bidError(error));
26524    }
26525  }
26526  bidError(e) {
26527    const msg = String(e?.message || e || '');
26528      if (/No est(?:á|a|\?)s autorizado para ofertar/i.test(msg)) return 'No estás autorizado para ofertar en lotes de este vendedor. Pedí autorización antes de ofertar.';
26529      return 'No se pudo enviar la oferta (' + msg + '). Verificá tu sesión.';
26530  }
26531  async submitBid() {
26532    const lot = this.currentLot(); if (!lot || this.state.bidSubmitting) return;
26533    if (lot.auctionStatus !== 'open' || lot.isClosed) {
26534      this.setState({ bidMessage: { text: 'La subasta está cerrada. No se pueden realizar más ofertas.', ok: false } });
26535      setTimeout(() => this.setState({ bidMessage: null }), 4000);
26536      return;
26537    }
26538    if (lot.sellerUid && this.state.user?.uid && lot.sellerUid === this.state.user.uid) {
26539      this.setState({ bidMessage: { text: 'No podés ofertar sobre un lote publicado por tu usuario.', ok: false } });
26540      setTimeout(() => this.setState({ bidMessage: null }), 4000);
26541      return;
26542    }
26543    const detailStanding = !!lot.currentBidBuyerUid && String(lot.currentBidBuyerUid) === String(this.state.user?.uid || '');
26544    if (detailStanding) {
26545      this.setState({ bidMessage: { text: '¡Vas ganando! No necesitás volver a ofertar.', ok: true } });
26546      setTimeout(() => this.setState({ bidMessage: null }), 4000);
26547      return;
26548    }
26549    const target = this.currentBid(lot) + this.bidAdj(lot.cert).offset;
26550    this.setState({ bidSubmitting: true, bidMessage: { text: 'Enviando oferta...', ok: true } });
26551    const token = await this.authToken();
26552    if (!this.state.lotsReal || !token) {
26553      this.setState({ bidSubmitting: false, bidMessage: { text: token ?'No se puede ofertar sobre un lote no sincronizado.' : 'Iniciá sesión para enviar una oferta.', ok: false } });
26554      setTimeout(() => this.setState({ bidMessage: null }), 4000);
26555      return;
26556    }
26557    try {
26558      await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(lot.cert), { method: 'POST', body: JSON.stringify({ amount: target }) });
26559      this.showRoslotBidSuccess(lot.cert, target);
26560      this.setState({ bidsPlaced: { ...this.state.bidsPlaced, [lot.cert]: target }, bidSubmitting: false, bidMessage: { text: 'Oferta confirmada por ' + this.fmtARS(target) + '.', ok: true } });
26561      this.fetchLotBids(lot.cert);
26562      this.fetchMyBids();
26563    } catch (e) {
26564      this.setState({ bidSubmitting: false, bidMessage: { text: this.bidError(e), ok: false } });
26565    }
26566    setTimeout(() => this.setState({ bidMessage: null }), 4000);
26567  }
26568  async closeRoslotLot(cert) {
26569    const safeCert = String(cert || '').trim();
26570    if (!safeCert || this.state.lotClosingCert) return;
26571    const lot = (this.state.lots || []).find((item) => String(item.cert) === safeCert)
26572      || (this.state.myLots || []).find((item) => String(item.cert) === safeCert);
26573    const ownUid = String(this.state.user?.uid || '');
26574    const isOwnLot = !!(lot?.sellerUid && ownUid && String(lot.sellerUid) === ownUid);
26575    const offers = this.state.lotBids?.[safeCert] || [];
26576    const hasOffer = !!(lot?.currentBidId || lot?.currentBidBuyerUid || Number(lot?.bidCount) > 0 || offers.length > 0);
26577    if (!lot || !isOwnLot) {
26578      this.showToast('Solo el dueño del lote puede bajar el martillo.');
26579      return;
26580    }
26581    if (lot.auctionStatus !== 'open') {
26582      this.showToast('Este lote ya no está abierto.');
26583      return;
26584    }
26585    if (!hasOffer) {
26586      this.showToast('Todavía no hay una oferta para adjudicar.');
26587      return;
26588    }
26589    const leadingAmount = Math.max(Number(lot.currentBidAmount) || 0, ...offers.map((bid) => Number(bid.amount) || 0));
26590    const accepted = window.confirm(
26591      '¿Bajar el martillo del lote ' + safeCert + '?\n\nLa subasta se cerrará ahora y la oferta líder'
26592      + (leadingAmount ?' de ' + this.fmtARS(leadingAmount) : '')
26593      + ' quedará como ganadora. Esta acción no se puede deshacer.'
26594    );
26595    if (!accepted) return;
26596    this.setState({ lotClosingCert: safeCert, bidMessage: { text: 'Cerrando el lote y adjudicando la oferta líder…', ok: true } });
26597    try {
26598      await this.apiFetch('/api/Lots/close/' + encodeURIComponent(safe
26598Cert), { method: 'POST' });
26599      this.showToast('Martillo bajado. El lote ' + safeCert + ' quedó vendido.');
26600      this.setState((previous) => {
26601        const markSold = (item) => String(item?.cert || '') === safeCert
26602          ? { ...item, auctionStatus: 'sold', status: 'Vendido', isClosed: true, hasOffers: true, soldAmount: leadingAmount || Number(item.currentBidAmount) || Number(item.soldAmount) || 0 }
26603          : item;
26604        return {
26605          lotClosingCert: '',
26606          closedRoslotLots: { ...(previous.closedRoslotLots || {}), [safeCert]: { amount: leadingAmount } },
26607          lots: (previous.lots || []).map(markSold),
26608          myLots: (previous.myLots || []).map(markSold),
26609          bidMessage: { text: 'Subasta cerrada. El lote quedó vendido.', ok: true },
26610        };
26611      });
26612      await Promise.all([
26613        this.fetchLots(),
26614        this.fetchMyLots(),
26615        this.fetchLotBids(safeCert),
26616        this.fetchMyBids(),
26617      ]);
26618      await this.fetchRoslotLotByCert(safeCert);
26619    } catch (error) {
26620      this.setState({ lotClosingCert: '', bidMessage: { text: error?.message || 'No se pudo cerrar el lote.', ok: false } });
26621    }
26622    setTimeout(() => this.setState({ bidMessage: null }), 4000);
26623  }
26624  toggleFilter(key, value) {
26625    const f = this.state.poolFilters; const list = f[key];
26626    const next = list.includes(value) ?list.filter((v) => v !== value) : [...list, value];
26627    this.setState({ poolFilters: { ...f, [key]: next } });
26628  }
26629  renderVals() {
26630    const s = this.state;
26631    const anim = this.props.animaciones || true;
26632    const authMode = s.authMode;
26633    const accountAssistantStep = Math.max(0, Math.min(5, Number(s.accountAssistantStep) || 0));
26634    const accountAssistantMethod = String(s.accountAssistantMethod || '');
26635    const accountAssistantEmailActive = !!s.accountAssistantOpen && accountAssistantMethod === 'email';
26636    const accountAssistantEmailContent = {
26637      1: { title: 'Paso 1: tu nombre', text: 'Escribí tu nombre y apellido en el campo que quedó iluminado.', tip: 'Usá tu nombre real. Así podremos identificarte y ayudarte si alguna vez necesitás recuperar el acceso.' },
26638      2: { title: 'Paso 2: tu celular', text: 'Escribí el número de tu teléfono móvil. Ya dejamos colocado el código de Argentina (+54).', tip: 'Ejemplo: +54 9 341 000 0000. Lo guardaremos como teléfono de contacto.' },
26639      3: { title: 'Paso 3: tu correo', text: 'Ahora escribí una dirección de correo que uses y puedas abrir.', tip: 'Revisá que no tenga espacios y que incluya el signo @. Este correo será tu usuario para ingresar.' },
26640      4: { title: 'Paso 4: una contraseña', text: 'Creá una contraseña que recuerdes, pero que otra persona no pueda adivinar.', tip: 'Debe tener al menos 8 caracteres y una letra mayúscula. Podés tocar el ojito para comprobar lo que escribiste.' },
26641      5: { title: 'Paso 5: revisar y crear', text: 'Ya completaste todos los datos. Si están correctos, tocá “Crear mi cuenta”.', tip: 'Con tu teléfono de contacto guardado podrás continuar.' },
26642    };
26643    const accountAssistantContent = accountAssistantMethod === 'google'
26644      ?{ title: 'Crear la cuenta con Google', text: 'Esta es la opción más sencilla si ya usás Gmail o tenés una cuenta de Google en el teléfono.', tip: 'Tocá “Continuar con Google” y elegí tu cuenta. Rosgan hará el resto; no necesitás inventar otra contraseña.' }
26645      : accountAssistantMethod === 'email'
26646        ?accountAssistantEmailContent[accountAssistantStep]
26647        : { title: 'Vamos a crear tu cuenta', text: 'Primero elegí la forma que te resulte más cómoda. No te preocupes: te voy a indicar exactamente qué tocar.', tip: 'Si usás Gmail, Google suele ser la opción más rápida. También podés registrarte con cualquier correo y una contraseña.' };
26648    const ferias = (s.ferias || []).filter((item) => !this.isExcludedRemate(item));
26649    const now = new Date(); const todayKey = now.toDateString();
26650    const sorted = [...ferias].sort((a, b) => (this.feriaDate(a) || 0) - (this.feriaDate(b) || 0));
26651    const upcoming = sorted.filter((f) => this.homeAgendaIncludes(this.feriaDate(f), now));
26652    const todays = upcoming.filter((f) => this.feriaDate(f)?.toDateString() === todayKey);
26653    const hero = todays[0] || upcoming[0] || ferias[0];
26654    const heroIsLive = !!todays.length;
26655    const p = anim ?(s.statProgress || (s.route === 'home' ?1 : 0)) : 1;
26656    const fmt = (n) =>
26656 Math.round(n).toLocaleString('es-AR');
26657    const accents = ['#002e5f', '#e63946', '#0a5ca8', '#1f7a63', '#8a5a2b', '#5b3fa8'];
26658    const CONS_ORDER = ['Alzaga Unzué', 'Ildarraz Hnos.', 'Aguirre Vazquez', 'Colombo y Colombo', 'Coop. G. Lehmann', 'Pastore', 'Etchevehere', 'Ferialvarez', 'Ganaderos de Ceres', 'Rigran'];
26659    // Official public destinations for the home cards (independent of internal channels).
26660    const CONS_WEBS = {
26661      'Alzaga Unzué': 'https://www.alzagaunzue.com/',
26662      'Ildarraz Hnos.': 'https://ildarraz.com.ar/',
26663      'Aguirre Vazquez': 'https://aguirrevazquez.com.ar/',
26664      'Colombo y Colombo': 'https://www.colomboycolombo.com.ar/',
26665      'Coop. G. Lehmann': 'https://cooperativalehmann.coop/',
26666      // Their public link page remains available while epastore.com.ar is unavailable.
26667      'Pastore': 'https://linktr.ee/PastoreConsignataria',
26668      'Etchevehere': 'https://etchevehere-rural.com.ar/',
26669      'Ferialvarez': 'https://www.ferialvarez.com.ar/',
26670      'Ganaderos de Ceres': 'https://www.ganaderosdeceres.com.ar/',
26671      'Rigran': 'https://rigran.com.ar/',
26672    };
26673    const apiCons = s.consignatarias || [];
26674    const consList = apiCons.slice(0, 10).map((c, i) => ({ ...c, name: CONS_ORDER[i] || (c.name || c.nombre || c.slug) }));
26675    const vids = (s.videos || []).slice(0, 8);
26676    const homeIndexCards = this.indexCards();
26677    const esFecha = (d) => { const dt = new Date(d); return isNaN(dt) ?'' : dt.toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }); };
26678    const esCorta = (d) => { const dt = new Date(d); return isNaN(dt) ?'' : dt.toLocaleDateString('es-AR', { day: '2-digit', month: 'short' }).replace('.', ''); };
26679    const loteRaw = (s.loteNoticias || []).filter((n) => n.isActive !== false).slice().sort((a, b) => new Date(b.fecha || b.createdAt) - new Date(a.fecha || a.createdAt));
26680    const loteCards = loteRaw.slice(0, 6).map((n, i) => ({
26681      title: n.titulo || 'Lote de noticias', href: n.url || n.publicUrl || '#',
26682      date: esFecha(n.fecha), day: esCorta(n.fecha).split(' ')[0] || '', mon: (esCorta(n.fecha).split(' ')[1] || '').toUpperCase(),
26683      featured: i === 0, badge: 'Informe', target: '_blank',
26684    }));
26685    const noticiaRaw = (s.noticias || []).filter((n) => n.isPublished !== false).slice().sort((a, b) => new Date(b.publicationDate) - new Date(a.publicationDate));
26686    const noticiaLead = noticiaRaw[0] ? {
26687      title: noticiaRaw[0].title, href: noticiaRaw[0].url || '#', category: (noticiaRaw[0].category || 'Noticias').trim(),
26688      excerpt: (noticiaRaw[0].excerpt || '').trim(), date: esFecha(noticiaRaw[0].publicationDate),
26689      hero: noticiaRaw[0].heroImageUrl || 'assets/feria.png',
26690    } : null;
26691    const noticiaCards = noticiaRaw.slice(1, 9).map((n) => ({
26692      title: n.title, href: n.url || '#', category: (n.category || 'Noticias').trim(),
26693      excerpt: (n.excerpt || '').trim(), date: esFecha(n.publicationDate), hero: n.heroImageUrl || 'assets/feria.png',
26694    }));
26695    this._notiCount = noticiaCards.length;
26696    const notiIdx = noticiaCards.length ?((s.notiSlide % noticiaCards.length) + noticiaCards.length) % noticiaCards.length : 0;
26697    const notiCurrent = noticiaCards[notiIdx] || null;
26698    const notiDots = noticiaCards.map((_, i) => ({
26699      go: () => this.setState({ notiSlide: i }),
26700      w: i === notiIdx ?'22px' : '7px', bg: i === notiIdx ?'#e63946' : '#c9d5e2',
26701    }));
26702    const navDefs = [
26703      { label: 'Inicio', route: 'home', path: 'M3 10.5L12 3l9 7.5M5 9.5V21h5v-6h4v6h5V9.5' },
26704      { label: 'Roslot', route: 'pool', path: 'M20.6 13.4l-7.2 7.2a2 2 0 0 1-2.8 0l-8-8V4a1 1 0 0 1 1-1h8.6l8.4 8.4a2 2 0 0 1 0 2.9zM7.6 7.6h.01' },
26705      { label: 'Remates', route: 'remates', path: 'M23 7l-7 5 7 5V7zM3 5h13v14H3z' },
26706      { label: 'Cabañas', route: 'cabanas', path: 'M4 20V9l8-5 8 5v11M8 20v-6h8v6M9 9h.01M15 9h.01' },
26707      { label: 'Feria', route: 'feria', path: 'M4.9 4.9a10 10 0 0 0 0 14.2M19.1 4.9a10 10 0 0 1 0 14.2M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M12.9 12a.9.9 0 1 1-1.8 0 .9.9 0 0 1 1.8 0z' },
26708      { label: 'Estadísticas', route: 'mercado', path: 'M4 19V5M4 19h16M8 16l3-4 3 2 4-6' },
26709      { label: 'Perfil', route: 'profile', path: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z' },
26710    ].filter((item) => {
26711      if (item.route === 'home') return true;
26712      if (!s.user) return ['remates', 'cabanas'].includes(item.route);
26713      if (item.route === 'pool') return this.canAccessRoslot(s.apiUser);
26714      if (item.route === 'feria') return this.canAccessFeria(s.apiUser);
26715      if (item.route === 'mercado') return this.canAccessStatistics(s.apiUser);
26716      return true;
26717    });
26718    const activeNav = (s.route === 'lot' || s.route === 'load' || s.route === 'roslotSummary') ?'pool' : ((s.route === 'remateDetail' || s.route === 'rosganResults' || s.route === 'rosganSalesOrder') ?'remates' : ((s.route === 'cabanaDetail' || s.route === 'cabanaAuditor' || s.route === 'cabanaPrebidPanel' || s.route === 'cabanaLotDetail' || s.route === 'cabanaLotCreate' || s.route === 'cabanaStats' || s.route === 'cabanaCommercial' || s.route === 'cabanaPriceBoard' || s.route === 'cabanaSalesOrder' || s.route === 'remateConfig') ?'cabanas' : s.route));
26719    const rawNotifications = Array.isArray(s.notificationsItems) ?s.notificationsItems : [];
26720    const unreadNotifications = rawNotifications.filter((notification) => !notification.read).length;
26721    const notificationTime = (value) => {
26722      const date = value ?new Date(value) : null;
26723      if (!date || Number.isNaN(date.getTime())) return '';
26724      const elapsed = Math.max(0, Date.now() - date.getTime());
26725      if (elapsed < 60000) return 'Ahora';
26726      if (elapsed < 3600000) return `Hace ${Math.floor(elapsed / 60000)} min`;
26727      if (date.toDateString() === new Date().toDateString()) {
26728        return date.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' });
26729      }
26730      return date.toLocaleDateString('es-AR', { day: '2-digit', month: 'short' });
26731    };
26732    const formatNotification = (notification) => {
26733      const type = String(notification.type || '');
26734      const safeCreditContent = type === 'roslot_credit_granted'
26735        ?{ title: 'Autorización de compra habilitada', body: 'Tu autorización para operar en Roslot fue habilitada.' }
26736        : type === 'roslot_credit_updated'
26737          ?{ title: 'Autorización de compra actualizada', body: 'Tu autorización para operar en Roslot fue actualizada.' }
26738          : type === 'roslot_credit_revoked'
26739            ?{ title: 'Autorización de compra desactivada', body: 'Tu autorización para operar en Roslot fue desactivada.' }
26740            : null;
26741      const notificationBody = String(notification.body || '');
26742      const privateBidAmount = notificationBody.match(/\$\s?[\d.]+(?:,\d+)?/)?.[0] || '';
26743      const privateBidCert = String(notification.lotCert || notification.cert || notification.data?.cert || notificationBody.match(/lote\s+([A-Za-z0-9_-]+)/i)?.[1] || '');
26744      const safeBidContent = type === 'roslot_bid_received'
26745        ?{
26746          title: 'Recibiste una nueva oferta',
26747          body: 'Oferta reservada' + (privateBidAmount ?' por ' + privateBidAmount : '') + (privateBidCert ?' en el lote ' + privateBidCert : '') + '.',
26748        }
26749        : null;
26750      const legacyLotCreated = type === 'remate_lot_workflow'
26751        ? notificationBody.match(/^(.+?) pasó (el certificado|los certificados) (.+?) a Cargado en (.+)$/)
26752        : null;
26753      const lotCreatedContent = legacyLotCreated ? {
26754        title: 'Nuevo lote cargado',
26755        body: legacyLotCreated[1] + ' cargó ' + (legacyLotCreated[2] === 'el certificado' ? 'un lote (certificado ' : 'lotes (certificados ') + legacyLotCreated[3] + ') en ' + legacyLotCreated[4],
26756      } : null;
26757      const safeContent = safeCreditContent || safeBidContent || lotCreatedContent;
26758      const visual = type === 'roslot_outbid' || type === 'roslot_lost'
26759        ?{ icon: 'trending-down', color: '#c92b38', bg: '#ffe7ea' }
26760        : type === 'cabana_prebid_outbid'
26761          ?{ icon: 'trending-up', color: '#c92b38', bg: '#ffe7ea' }
26762          : type === 'cabana_prebid_won'
26763            ?{ icon: 'trophy', color: '#14745f', bg: '#ddf2eb' }
26764            : type === 'cabana_prebid_received'
26765              ?{ icon: 'gavel', color: '#07518f', bg: '#dcecff' }
26766        : type === 'roslot_won'
26767          ?{ icon: 'trophy', color: '#14745f', bg: '#ddf2eb' }
26768          : type === 'roslot_bid_received'
26769            ?{ icon: 'gavel', color: '#07518f', bg: '#dcecff' }
26770            : type === 'roslot_auction_closing'
26771              ?{ icon: 'clock-3', color: '#a76200', bg: '#fff0d5' }
26772              : type === 'account_role_changed'
26773                ?{ icon: 'user-cog', color: '#07518f', bg: '#dcecff' }
26774              : type.includes('credit')
26775                ?{ icon: 'badge-dollar-sign', color: '#6445c2', bg: '#ece7ff' }
26776                : { icon: 'bell', color: '#07518f', bg: '#dcecff' };
26777      const companyRef = String(notification.consignatariaId || '');
26778      const whiteLogo = this.consWhiteLogo(companyRef);
26779      const companyLogo = whiteLogo || this.consLogo(companyRef) || (type === 'remate_heads_milestone' ? '/assets/rosgan-logo-blue.png' : '');
26780      return {
26781        ...notification,
26782        title: safeContent?.title || notification.title || 'Roslot',
26783        body: safeContent?.body || notification.body || 'Tenés una nueva notificación.',
26784        time: notificationTime(notification.createdAt),
26785        bg: notification.read ?'#fff' : '#eef6ff',
26786        icon: companyLogo || `assets/icons/lucide/${visual.icon}.svg`,
26787        iconBg: companyLogo ? (whiteLogo ? this.consBrandColor(companyRef) : '#fff') : (notification.read ? '#eef2f6' : visual.bg),
26788        iconSize: companyLogo ? '28px' : '17px',
26789        historyIconSize: companyLogo ? '32px' : '19px',
26790        iconRadius: companyLogo ? '10px' : '50%',
26791        iconLabel: companyLogo ? 'Consignataria' : '',
26792        dotBg: notification.read ?'transparent' : visual.color,
26793        status: notification.read ?'Leída' : 'Nueva',
26794        statusBg: notification.read ?'#edf1f5' : visual.bg,
26795        statusColor: notification.read ?'#65758a' : visual.color,
26796        open: () => this.openNotification(notification),
26797      };
26798    };
26799    const notificationItems = rawNotifications.map(formatNotification);
26800    const notificationHistoryItems = (Array.isArray(s.notificationHistory) ?s.notificationHistory : []).map(formatNotification);
26801    const roslotLiveToastItem = s.roslotLiveToast ?formatNotification(s.roslotLiveToast) : null;
26802    // ---- POOL ----
26803    const lots = s.lots || [];
26804    const q = s.poolQuery.trim().toLowerCase();
26805    const f = s.poolFilters;
26806    const baDayFormatter = new Intl.DateTimeFormat('en-CA', {
26807      timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit',
26808    });
26809    const baTodayParts = baDayFormatter.formatToParts(new Date());
26810    const baPart = (type) => baTodayParts.find((part) => part.type === type)?.value || '';
26811    const baTodayKey = baPart('year') + '-' + baPart('month') + '-' + baPart('day');
26812    const auctionDay = (lot) => String(lot.fecha || lot.raw?.fechaSubasta || '').slice(0, 10);
26813    const isPoolDay = (lot, view = s.poolStatus) => {
26814      const day = auctionDay(lot);
26815      return view === 'upcoming' ?day > baTodayKey : day === baTodayKey;
26816    };
26817    const poolEligibleLots = lots.filter((lot) => lot.status !== 'Caducado');
26818    const filterSourceLots = poolEligibleLots.filter((lot) => isPoolDay(lot));
26819    const todayCategoryMap = new Map();
26820    lots.filter((lot) => auctionDay(lot) === baTodayKey).forEach((lot) => {
26821      const label = String(lot.categoria || 'Sin categoría').trim() || 'Sin categoría';
26822      const key = label.toLocaleLowerCase('es-AR');
26823      const current = todayCategoryMap.get(key) || { label, lots: 0, heads: 0 };
26824      current.lots += 1;
26825      current.heads += Math.max(0, Number(lot.cabezas) || 0);
26826      todayCategoryMap.set(key, current);
26827    });
26828    const roslotDailyCategories = Array.from(todayCategoryMap.values()).map((category) => ({
26829      label: category.label,
26830      quantity: (category.heads || category.lots).toLocaleString('es-AR'),
26831      unit: category.heads ?'cabezas' : (category.lots === 1 ?'lote' : 'lotes'),
26832      lotsLabel: category.lots === 1 ?'1 lote' : category.lots.toLocaleString('es-AR') + ' lotes',
26833    }));
26834    let filtered = filterSourceLots.filter((l) =>
26835      (!q || [l.categoria, l.origen, l.provincia, l.cert, ...(l.razas || [])].join(' ').toLowerCase().includes(q))
26836      && (!f.category.length || f.category.includes(l.categoria))
26837      && (!f.destination.length || f.destination.includes(l.destino))
26838      && (!f.fattening.length || f.fattening.includes(l.engorde))
26839      && (!f.province.length || f.province.includes(l.provincia))
26840      && (!f.china || l.aptoChina));
26841    if (s.poolSort === 'precio-asc') filtered = [...filtered].sort((a, b) => a.baseNum - b.baseNum);
26842    if (s.poolSort === 'precio-desc') filtered = [...filtered].sort((a, b) => b.baseNum - a.baseNum);
26843    if (s.poolSort === 'peso') filtered = [...filtered].sort((a, b) => b.pesoNum - a.pesoNum);
26844    if (s.poolSort === 'cabezas') filtered = [...filtered].sort((a, b) => (Number(b.cabezas) || 0) - (Number(a.cabezas) || 0));
26845    const failedBidPattern = /(reject|rechaz|declin|lost)/i;
26846    const lotByCert = Object.fromEntries(lots.map((lot) => [lot.cert, lot]));
26847    const ownBidByCert = {};
26848    (s.myBids || []).forEach((bid) => {
26849      const bidLot = lotByCert[bid.cert];
26850      if (!bid.cert || bidLot?.auctionStatus !== 'open' || failedBidPattern.test(bid.status)) return;
26851      const current = ownBidByCert[bid.cert];
26852      const amount = Math.max(Number(current?.amount) || 0, Number(bid.amount) || 0);
26853      const topAmount = Math.max(Number(current?.topAmount) || 0, Number(bid.topAmount) || 0);
26854      const useBidFlag = !current || Number(bid.amount) >= Number(current.amount);
26855      const accepted = /(approv|aprob|accept)/i.test(bid.status);
26856      const outbid = /(outbid|superad)/i.test(bid.status);
26857      ownBidByCert[bid.cert] = { amount, topAmount, isWinning: outbid ?false : (accepted ?true : (useBidFlag ?bid.isWinning : current.isWinning)) };
26858    });
26859    Object.entries(s.bidsPlaced || {}).forEach(([cert, amount]) => {
26860      if (lotByCert[cert]?.auctionStatus !== 'open') return;
26861      const current = ownBidByCert[cert] || {};
26862      ownBidByCert[cert] = {
26863        amount: Math.max(Number(current.amount) || 0, Number(amount) || 0),
26864        topAmount: Number(current.topAmount) || 0,
26865        isWinning: current.isWinning || null,
26866      };
26867    });
26868    const standingByCert = {};
26869    Object.entries(ownBidByCert).forEach(([cert, bid]) => {
26870      const bidLot = lotByCert[cert];
26871      const authoritativeTop = Number(bidLot?.currentBidAmount) || 0;
26872      const authoritativeBuyer = String(bidLot?.currentBidBuyerUid || '');
26873      const signedInUid = String(s.user?.uid || '');
26874      const isWinning = authoritativeBuyer
26875        ?!!signedInUid && authoritativeBuyer === signedInUid
26876        : bid.isWinning;
26877      const effectiveMyAmount = isWinning && authoritativeBuyer
26878        ? Math.max(Number(bid.amount) || 0, authoritativeTop)
26879        : Number(bid.amount) || 0;
26880      const standing = this.computeStanding(cert, effectiveMyAmount, isWinning, Math.max(Number(bid.topAmount) || 0, authoritativeTop));
26881      if (standing) standingByCert[cert] = standing;
26882    });
26883    const activeStandingCount = filtered.filter((lot) => !!standingByCert[lot.cert]).length;
26884    const roslotCanBuy = !!s.user && this.canUseBuyerData(s.apiUser);
26885    const roslotCanSell = !!s.user && this.canManageSellerData(s.apiUser);
26886    const ownUid = String(s.user?.uid || '');
26887    const ownLots = Array.isArray(s.myLots) ?s.myLots : [];
26888    const ownLotCerts = new Set(ownLots.map((item) =>
26888 String(item.cert || '')).filter(Boolean));
26889    const pendingOwnBids = (s.pendingBids || [])
26890      .filter((bid) => !ownLotCerts.size || ownLotCerts.has(String(bid.cert || '')))
26891      .filter((bid) => (lotByCert[bid.cert] || ownLots.find((item) => item.cert === bid.cert))?.auctionStatus === 'open');
26892    const operationLotTitle = (lot, fallbackCategory = '', cert = '') => {
26893      const category = String(lot?.categoria || fallbackCategory || '').trim();
26894      const quantityRaw = lot?.cabezas ?? lot?.totalCabezas ?? lot?.cantidadTotal;
26895      const quantity = Number(quantityRaw) > 0 ?Number(quantityRaw).toLocaleString('es-AR') : '';
26896      const weightNumber = Number(lot?.pesoNum ?? lot?.pesoPromedio) || 0;
26897      const weight = weightNumber > 0 ?weightNumber.toLocaleString('es-AR') + ' kg' : (/\d/.test(String(lot?.peso || '')) ?String(lot.peso) : '');
26898      const location = String(lot?.origen || [lot?.localidad, lot?.provincia].filter(Boolean).join(', ')).trim();
26899      const subject = [quantity, category].filter(Boolean).join(' ') || ('Lote ' + cert);
26900      return subject + (weight ?' de ' + weight : '') + (location && !/pendiente/i.test(location) ?' en ' + location : '');
26901    };
26902    const buyerOperationRows = Object.entries(ownBidByCert).map(([cert, bid]) => {
26903      const operationLot = lotByCert[cert];
26904      const standing = standingByCert[cert] || this.computeStanding(cert, bid.amount, bid.isWinning, bid.topAmount);
26905      const liveTop = Math.max(0, Number(operationLot?.currentBidAmount) || 0, Number(bid.topAmount) || 0, Number(bid.amount) || 0, ...(s.lotBids?.[cert] || []).map((item) => Number(item.amount) || 0));
26906      const canBidAgain = standing?.key === 'outbid' && operationLot?.auctionStatus === 'open' && (!operationLot?.sellerUid || operationLot.sellerUid !== ownUid);
26907      const isOutbid = standing?.key === 'outbid';
26908      return {
26909        priority: isOutbid ?0 : 2,
26910        kind: 'Compra', kindBg: '#e9f3ff', kindColor: '#075ca8', cert,
26911        title: operationLotTitle(operationLot, '', cert),
26912        meta: 'Lote ' + cert + ' · Seguimiento de tu posición en tiempo real',
26913        status: standing?.label || 'Oferta activa', statusColor: standing?.color || '#0a5ca8',
26914        canBid: !!canBidAgain, canApprove: false, disabled: s.quickBidCert === cert,
26915        bid20: () => this.quickBid(cert, liveTop, 20), bid50: () => this.quickBid(cert, liveTop, 50),
26916        open: () => this.openLot(cert), standingKey: standing?.key || '', liveTop,
26917        rowClass: 'is-buyer ' + (isOutbid ?'is-outbid' : 'is-winning'),
26918        isOutbid,
26919        showPrice: true,
26920        priceLabel: isOutbid ?'Oferta líder' : 'Vas ganando',
26921        priceValue: this.fmtARS(liveTop),
26922        priceDetail: 'Tu oferta ' + this.fmtARS(Number(bid.amount) || 0),
26923        bidSuccess: s.roslotBidSuccessFx?.cert === cert,
26924        bidSuccessAmount: s.roslotBidSuccessFx?.cert === cert ?this.fmtARS(s.roslotBidSuccessFx.amount) : '',
26925      };
26926    });
26927    const sellerPendingRows = pendingOwnBids.map((bid) => {
26928      const operationLot = lotByCert[bid.cert] || ownLots.find((item) => item.cert === bid.cert);
26929      return {
26930        priority: 1, kind: 'Venta', kindBg: '#e7f6f0', kindColor: '#14745f', cert: bid.cert,
26931        title: operationLotTitle(operationLot, bid.lotName, bid.cert),
26932        meta: 'Nueva oferta reservada por ' + this.fmtARS(bid.amount),
26933        status: 'Requiere aprobación', statusColor: '#b66b00',
26934        canBid: false, canApprove: true, disabled: s.bidActioning === bid.id,
26935        approve: () => this.actOnBid(bid.cert, bid.id, 'approve'), reject: () => this.actOnBid(bid.cert, bid.id, 'reject'),
26936        open: () => this.openLot(bid.cert), bidId: bid.id,
26937        rowClass: 'is-seller', showPrice: true, priceLabel: 'Nueva oferta', priceValue: this.fmtARS(bid.amount), priceDetail: 'Lote ' + bid.cert,
26938      };
26939    });
26940    const pendingCerts = new Set(sellerPendingRows.map((row) => row.cert));
26941    const sellerLotRows = ownLots.filter((item) => {
26942      const soldTodayOrJustClosed = item.auctionStatus === 'sold' && (auctionDay(item) === baTodayKey || !!s.closedRoslotLots?.[item.cert]);
26943      return (item.auctionStatus === 'open' || soldTodayOrJustClosed) && !pendingCerts.has(item.cert);
26944    }).map((item) => {
26945      const offers = s.lotBids?.[item.cert] || [];
26946      const isSold = item.auctionStatus === 'sold' || item.status === 'Vendido';
26947      const top = Math.max(Number(isSold ?item.soldAmount : item.currentBidAmount) || 0, Number(item.currentBidAmount) || 0, ...offers.map((bid) => Number(bid.amount) || 0), 0);
26948      const count = Math.max(Number(item.bidCount) || 0, offers.length);
26949      const canClose = isSold || !!(item.currentBidId || item.currentBidBuyerUid || count > 0);
26950      const closing = s.lotClosingCert === item.cert;
26951      return {
26952        priority: isSold ?4 : 3, kind: 'Venta', kindBg: '#e7f6f0', kindColor: '#14745f', cert: item.cert,
26953        title: operationLotTitle(item, '', item.cert),
26954        meta: 'Lote ' + item.cert + ' · ' + (isSold ?'Vendido' : count + (count === 1 ?' oferta' : ' ofertas')),
26955        status: isSold ?'Vendido' : count ?'Recibiendo ofertas' : 'Publicado', statusColor: isSold ?'#14745f' : count ?'#14745f' : '#65758a',
26956        canBid: false, canApprove: false, canClose, disabled: closing || isSold, open: () => this.openLot(item.cert),
26957        close: () => this.closeRoslotLot(item.cert), closeLabel: isSold ?'Subasta cerrada' : closing ?'Cerrando…' : 'Bajar el martillo',
26958        rowClass: 'is-seller', showPrice: top > 0, priceLabel: isSold ?'Precio final' : 'Oferta líder', priceValue: top ?this.fmtARS(top) : '', priceDetail: isSold ?'Lote vendido' : count + (count === 1 ?' oferta' : ' ofertas'),
26959      };
26960    });
26961    const roslotOperations = [...(roslotCanBuy ?buyerOperationRows : []), ...(roslotCanSell ?sellerPendingRows : []), ...(roslotCanSell ?sellerLotRows : [])]
26962      .sort((a, b) => a.priority - b.priority || String(a.cert).localeCompare(String(b.cert), 'es'))
26963      .slice(0, 6);
26964    const buyerWinningCount = buyerOperationRows.filter((row) => row.standingKey === 'winning').length;
26965    const buyerOutbidCount = buyerOperationRows.filter((row) => row.standingKey === 'outbid').length;
26966    const buyerWonCerts = new Set((s.myBids || []).filter((bid) => /(won|adjud|ganad)/i.test(bid.status)).map((bid) => bid.cert));
26967    const sellerActiveLots = ownLots.filter((item) => item.auctionStatus === 'open').length;
26968    const sellerSoldLots = ownLots.filter((item) => item.auctionStatus === 'sold').length;
26969    const sellerReceivedOffers = ownLots.reduce((sum, item) => sum + (Number(item.bidCount) || (s.lotBids?.[item.cert] || []).length || 0), 0);
26970    const roslotKpis = roslotCanBuy && roslotCanSell
26971      ?[
26972        { label: 'Posiciones activas', value: buyerOperationRows.length, detail: 'compras en curso', color: '#075ca8' },
26973        { label: 'Vas ganando', value: buyerWinningCount, detail: buyerOutbidCount + ' superadas', color: '#14745f' },
26974        { label: 'Ofertas recibidas', value: sellerReceivedOffers, detail: sellerActiveLots + ' lotes activos', color: '#075ca8' },
26975        { label: 'Por resolver', value: buyerOutbidCount + pendingOwnBids.length, detail: 'requieren acción', color: '#c92b38' },
26976      ]
26977      : roslotCanSell
26978        ?[
26979          { label: 'Lotes activos', value: sellerActiveLots, detail: ownLots.length + ' publicados', color: '#075ca8' },
26980          { label: 'Ofertas recibidas', value: sellerReceivedOffers, detail: 'sobre tus lotes', color: '#14745f' },
26981          { label: 'Por aprobar', value: pendingOwnBids.length, detail: 'requieren acción', color: '#c92b38' },
26982          { label: 'Vendidos', value: sellerSoldLots, detail: 'operaciones cerradas', color: '#6b4bb5' },
26983        ]
26984        :[
26985          { label: 'Ofertas activas', value: buyerOperationRows.length, detail: 'posiciones abiertas', color: '#075ca8' },
26986          { label: 'Vas ganando', value: buyerWinningCount, detail: 'liderando ahora', color: '#14745f' },
26987          { label: 'Te superaron', value: buyerOutbidCount, detail: 'requieren acción', color: '#c92b38' },
26988          { label: 'Lotes ganados', value: buyerWonCerts.size, detail: 'operaciones cerradas', color: '#6b4bb5' },
26989        ];
26990    const syntheticActivity = [
26991      ...buyerOperationRows.filter((row) => row.standingKey === 'outbid').map((row) => ({
26992        key: 'outbid:' + row.cert, cert: row.cert, title: 'Te superaron en el lote ' + row.cert,
26993        body: 'La oferta líder es ' + this.fmtARS(row.liveTop) + '. Podés volver a ofertar desde tu panel.', time: 'Ahora', status: 'Atención', statusColor: '#c92b38',
26994        icon: 'assets/icons/lucide/trending-down.svg', iconBg: '#ffe7ea', open: row.open,
26995      })),
26996      ...sellerPendingRows.map((row) => ({
26997        key: 'received:' + row.cert, cert: row.cert, title: 'Recibiste una oferta en el lote ' + row.cert,
26998        body: row.meta, time: 'Ahora', status: 'Por resolver', statusColor: '#b66b00',
26999        icon: 'assets/icons/lucide/gavel.svg', iconBg: '#dcecff', open: row.open,
27000      })),
27001    ];
27002    const activityKey = (event) => {
27003      const type = String(event.type || '');
27004      const directCert = String(event.cert || event.lotCert || event.data?.cert || '');
27005      const textCert = [event.title, event.body, event.href].filter(
27005Boolean).join(' ').match(/lote\s+([A-Za-z0-9_-]+)/i)?.[1] || '';
27006      const cert = directCert || textCert;
27007      if (type === 'roslot_outbid' || /superaron/i.test(event.title || '')) return 'outbid:' + cert;
27008      if (type === 'roslot_bid_received' || /recibiste una oferta/i.test(event.title || '')) return 'received:' + cert;
27009      return 'notification:' + String(event.id || [event.title, event.createdAt].filter(Boolean).join(':'));
27010    };
27011    const activityDayKey = (value) => {
27012      const date = value ?new Date(value) : null;
27013      if (!date || Number.isNaN(date.getTime())) return '';
27014      const parts = baDayFormatter.formatToParts(date);
27015      const part = (type) => parts.find((item) => item.type === type)?.value || '';
27016      return part('year') + '-' + part('month') + '-' + part('day');
27017    };
27018    const towerNotificationSeen = new Set();
27019    const towerNotifications = [...notificationItems, ...notificationHistoryItems]
27020      .filter((event) => activityDayKey(event.createdAt) === baTodayKey)
27021      .filter((event) => {
27022        const key = String(event.id || activityKey(event));
27023        return !towerNotificationSeen.has(key) && towerNotificationSeen.add(key);
27024      });
27025    const towerSemanticKeys = new Set(towerNotifications.map(activityKey));
27026    const pulseKey = String(s.roslotLivePulseId || '');
27027    const towerHistory = towerNotifications.map((event) => {
27028      const notificationKey = String(event.id || [event.type, event.createdAt, event.title].filter(Boolean).join(':'));
27029      return { ...event, key: 'notification:' + notificationKey, rowClass: notificationKey === pulseKey ?'is-arriving' : '' };
27030    });
27031    const activitySeen = new Set();
27032    const roslotActivity = [...towerHistory, ...syntheticActivity.filter((event) => !towerSemanticKeys.has(event.key)).map((event) => ({ ...event, rowClass: '' }))]
27033      .filter((event) => !activitySeen.has(event.key) && activitySeen.add(event.key))
27034      .slice(0, 100);
27035    const uniq = (arr) => [...new Set(arr.filter((v) => v && v !== 'Pendiente'))];
27036    const mkOptions = (key, values) => uniq(values).map((v) => ({
27037      label: v, toggle: () => this.toggleFilter(key, v),
27038      bg: f[key].includes(v) ?'#002e5f' : '#f2f6fb', color: f[key].includes(v) ?'#fff' : '#3d4c5f',
27039      border: f[key].includes(v) ?'#002e5f' : '#dde7f2',
27040    }));
27041    const activeFilterCount = f.category.length + f.destination.length + f.fattening.length + f.province.length + (f.china ?1 : 0);
27042    const statusDefs = [
27043      { key: 'today', label: 'Subasta de hoy' },
27044    ];
27045    const marketNow = Date.now();
27046    const marketTiming = this.roslotMarketTiming(lots, marketNow);
27047    const marketOpen = lots.some((lot) => lot.auctionStatus === 'open' && auctionDay(lot) === baTodayKey) || marketTiming.isOpen;
27048    const marketTarget = marketOpen ?(marketTiming.isOpen ?marketTiming.target : null) : marketTiming.target;
27049    const marketProgressValue = marketOpen && !marketTiming.isOpen ?100 : Math.round(Number(marketTiming.progress || 0) * 10) / 10;
27050    const marketRemainingLabel = (target) => {
27051      const seconds = Math.max(0, Math.ceil((target - marketNow) / 1000));
27052      const days = Math.floor(seconds / 86400);
27053      const hours = Math.floor((seconds % 86400) / 3600);
27054      const minutes = Math.floor((seconds % 3600) / 60);
27055      const secs = seconds % 60;
27056      if (days) return `${days} d ${String(hours).padStart(2, '0')} h`;
27057      return `${String(hours).padStart(2, '0')}
27057:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
27058    };
27059    const marketTargetLabel = marketTarget ?new Intl.DateTimeFormat('es-AR', {
27060      timeZone: 'America/Argentina/Buenos_Aires', weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
27061    }).format(new Date(marketTarget)).replace(',', ' ·') : '';
27062    const marketStatus = s.lotsLoading
27063      ?{ label: 'Consultando mercado', bg: 'rgba(10,92,168,0.08)', color: '#45627d', border: '#d7e5f2', icon: 'assets/icons/lucide/loader-circle.svg' }
27064      : marketOpen
27065        ?{ label: 'Mercado abierto', bg: 'rgba(31,122,99,0.12)', color: '#176c58', border: 'rgba(31,122,99,0.28)', icon: 'assets/icons/lucide/radio.svg' }
27066        : { label: 'Mercado cerrado', bg: 'rgba(101,117,138,0.1)', color: '#526174', border: '#d7e0ea', icon: 'assets/icons/lucide/lock-keyhole.svg' };
27067    const hoverIn = (e) => {
27068      const v = e.currentTarget.querySelector('[data-lot-preview]');
27069      if (v) {
27070        v.dataset.hoverActive = '1';
27071        v.style.opacity = '0';
27072        v.defaultMuted = true;
27073        v.muted = true;
27074        v.volume = 0;
27075        v.loop = true;
27076        v.playsInline = true;
27077        v.setAttribute('muted', '');
27078        v.setAttribute('loop', '');
27079        if (v.dataset.muteLocked !== '1') {
27080          v.dataset.muteLocked = '1';
27081          v.addEventListener('volumechange', () => {
27082            if (!v.muted || v.volume !== 0) {
27083              v.muted = true;
27084              v.volume = 0;
27085            }
27086          });
27087        }
27088        if (v.dataset.previewBound !== '1') {
27089          v.dataset.previewBound = '1';
27090          const showWhenReady = () => {
27091            if (v.dataset.hoverActive !== '1' || v.paused || v.readyState < 2) return;
27092            const revealFrame = () => {
27093              v.dataset.previewFramePending = '0';
27094              if (v.dataset.hoverActive === '1' && !v.paused) v.style.opacity = '1';
27095            };
27096            if (typeof v.requestVideoFrameCallback === 'function') {
27097              if (v.dataset.previewFramePending === '1') return;
27098              v.dataset.previewFramePending = '1';
27099              v.requestVideoFrameCallback(revealFrame);
27100            } else {
27101              revealFrame();
27102            }
27103          };
27104          const hideBrokenPreview = () => { v.style.opacity = '0'; };
27105          v.addEventListener('playing', showWhenReady);
27106          v.addEventListener('loadeddata', showWhenReady);
27107          v.addEventListener('error', hideBrokenPreview);
27108          v.addEventListener('emptied', hideBrokenPreview);
27109        }
27110        if (!v.src && v.dataset.src) v.src = v.dataset.src;
27111        const playback = v.play();
27112        if (playback?.catch) playback.catch(() => { v.style.opacity = '0'; });
27113      }
27114      const img = e.currentTarget.querySelector('[data-lot-img]');
27115      if (img) img.style.transform = 'scale(1.06)';
27116    };
27117    const hoverOut = (e) => { const v = e.currentTarget.querySelector('[data-lot-preview]'); if (v) { v.dataset.hoverActive = '0'; v.style.opacity = '0'; v.pause(); } const img = e.currentTarget.querySelector('[data-lot-img]'); if (img) img.style.transform = 'scale(1)'; };
27118    // ---- LOT DETAIL ----
27119    const lot = this.currentLot();
27120    const adj = lot ?this.bidAdj(lot.cert) : { increment: 20, offset: 20 };
27121    const curBid = lot ?this.currentBid(lot) : 0;
27122    const target = curBid + adj.offset;
27123    const selVideo = lot?.videos[Math.min(s.selectedVideo, Math.max((lot?.videos.length || 1) - 1, 0))];
27124    // ---- LOAD WIZARD ----
27125    const fm = this.form();
27126    const opt = (cat, key) => this.CATALOGS[cat].map(([id, label]) => ({ id, label, pick: () => this.setF(key, id),
27127      bg: fm[key] === id ?'#002e5f' : '#f2f6fb', color: fm[key] === id ?'#fff' : '#3d4c5f', border: fm[key] === id ?'#002e5f' : '#dde7f2' }));
27128    const wizFilledVideos = fm.videos.filter(Boolean);
27129    const canLoadLot = this.canManageSellerData(s.apiUser);
27130    const rc = s.remateConfigForm || this.emptyRemateConfig();
27131    const rcToggles = s.remateConfigToggles || this.emptyRemateConfigToggles();
27132    const rcOption = (value, label = value) => ({ value, label });
27133    const rcField = (key, label, kind = 'text', options = [], placeholder = '', help = '') => {
27134      const value = rc[key] || '';
27135      const isOpen = value === 'abierta';
27136      return { key, label, value, placeholder, help, options, isPlace: kind === 'place', isText: kind === 'text', isSelect: kind === 'select', isDate: kind === 'date', isTime: kind === 'time', isSwitch: kind === 'switch', switchState: isOpen ?'Abierta' : 'Cerrada', switchTrack: isOpen ?'#1f8a70' : '#cfd8e3', switchKnob: isOpen ?'20px' : '0px', on: (e) => this.setRemateConfigField(key, e.target.value, kind === 'text'), toggle: () => this.setRemateConfigField(key, isOpen ?'cerrada' : 'abierta') };
27137    };
27138    const rawConsignatarias = (Array.isArray(s.consignatarias) ?s.consignatarias : (Array.isArray(s.consignatarias?.items) ?s.c
27138onsignatarias.items : []))
27139      .filter((item) => !this.isExcludedConsignataria(item?.name || item?.nombre || item?.slug || item?.id));
27140    const remateConsignatarias = rawConsignatarias.map((item) => ({
27141      value: String(item?.id || item?.consignatariaId || item?.slug || this.rematePayloadKey(this.consName(item))),
27142      label: this.consName(item),
27143    })).filter((item) => item.value && item.label && !/reggi|gananor/i.test(item.label)).filter((item, index, all) => all.findIndex((candidate) => candidate.value === item.value) === index).sort((a, b) => a.label.localeCompare(b.label, 'es'));
27144    const lotSellerUid = String(lot?.sellerUid || '').trim();
27145    const lotBelongsToCurrentUser = !!lotSellerUid && lotSellerUid === String(s.user?.uid || '');
27146    const lotSellerRelation = (s.lotSellerConsignatarias || {})[lotSellerUid]
27147      || (lotBelongsToCurrentUser ?s.userConsignataria : null);
27148    const lotSellerConsignatariaRef = lotSellerRelation?.ref || lotSellerRelation?.id || lotSellerRelation?.slug || lotSellerRelation?.name || '';
27149    const lotConsignatariaRef = String(lot?.consignatariaId || lot?.consignatariaName || lotSellerConsignatariaRef || '').trim();
27150    const lotConsignatariaKey = this.consKey(lotConsignatariaRef);
27151    const lotConsignatariaIsGeneric = /^(productorrosgan|productor|vendedor|seller)$/.test(lotConsignatariaKey);
27152    const lotConsignatariaRecord = !lotConsignatariaIsGeneric && lotConsignatariaKey
27153      ?rawConsignatarias.find((item) => [item?.id, item?.consignatariaId, item?.slug, item?.name, item?.nombre]
27154        .some((value) => this.consKey(value) === lotConsignatariaKey))
27155      : null;
27156    const lotHasConsignataria = !!lotConsignatariaKey && !lotConsignatariaIsGeneric;
27157    const lotPublisherRelationResolved = !lotSellerUid
27158      || Object.prototype.hasOwnProperty.call(s.lotSellerConsignatarias || {}, lotSellerUid)
27159      || (lotBelongsToCurrentUser && !!s.userConsignataria);
27160    const lotPublisherPending = !!lot && !lotHasConsignataria && !lotPublisherRelationResolved;
27161    const lotPublisherName = lotHasConsignataria
27162      ?((lotConsignatariaRecord ?this.consName(lotConsignatariaRecord) : '') || lot?.consignatariaName || this.humanizeSlug(lotConsignatariaRef))
27163      : (lotPublisherPending ?'Cargando consignataria…' : 'CAF');
27164    const lotPublisherLogo = lotHasConsignataria
27165      ?(this.consLogo(lotConsignatariaRef) || this.consLogo(lotPublisherName) || lotConsignatariaRecord?.logoUrl || lotConsignatariaRecord?.logo || lotConsignatariaRecord?.imageUrl || this.consFullHeroLogo(lotPublisherName))
27166      : '';
27167    const lotPublisherUsesWhiteLogo = /\/consignatarias-full-white\//i.test(lotPublisherLogo);
27168    const remateProvincias = s.geoProvinces || [];
27169    const remateLocalidades = (s.geoLocs?.[rc.provincia] || []).map((item) => item.nombre);
27170    const remateConfigSections = [
27171      { step: '01', title: 'Datos del remate', fields: [
27172        rcField('titulo', 'Título', 'text', [], 'Nombre completo del remate'),
27173        rcField('cabezas', 'Cabezas estimadas', 'text', [], 'Ej. 1.500'),
27174        rcField('consignataria', 'Consignataria', 'select', [rcOption('', 'Seleccioná la consignataria'), ...remateConsignatarias.map((item) => rcOption(item.value, item.label))]),
27175        rcField('provincia', 'Provincia', 'select', ['', ...remateProvincias].map((value) => rcOption(value, value || (s.geoLoadingProv ?'Cargando provincias…' : 'Seleccioná la provincia')))),
27176        rcField('localidad', 'Localidad', 'place'), rcField('lugar', 'Lugar', 'text', [], 'Predio o establecimiento'),
27177      ] },
27178      { step: '02', title: 'Fecha y transmisión', fields: [rcField('fecha', 'Fecha', 'date'), rcField('hora', 'Hora', 'time'), rcField('urlStreaming', 'URL de streaming', 'text', [], 'https://...', 'Podés dejarla vacía y agregarla más adelante')] },
27179      { step: '03', title: 'Contacto del remate · Opcional', fields: [
27180        rcField('contactoNombre', 'Nombre', 'text', [], 'Nombre y apellido'),
27181        rcField('contactoTelefono', 'Teléfono', 'text', [], '+54 9 ...', 'Se utilizará para llamadas y consultas por WhatsApp'),
27182        rcField('contactoEmail', 'Email', 'text', [], '[email protected]'),
27183        rcField('contactoNotas', 'Notas', 'text', [], 'Información interna opcional'),
27184      ] },
27185      { step: '04', title: 'Carga de datos y lotes', fields: [
27186        rcField('cargaDatos', 'Carga de datos', 'switch'),
27187        rcField('cargaLotes', 'Habilitar carga de lotes', 'switch'),
27188      ] },
27189    ];
27190    const remateConfigOptions = [['videoFinal','Habilitar video final'],['preIndividual','Preoferta individual'],['apruebaAuto','Aprobar preofertas automáticamente'],['preAgrupada','Agrupar preofertas'],['agrupaCorrales','Agrupar corrales']].map(([key, label]) => { const on = !!rcToggles[key]; return { label, toggle: () => this.toggleRemateConfig(key), track: on ?'#0a5ca8' : '#cfdbe9', knob: on ?'16px' : '0px', bg: on ?'#f1f7fd' : '#f8fafc', border: on ?'#b8d5f2' : '#e3eaf2' }; });
27191    const preofferFields = [rcField('preInicioFecha', 'Inicio', 'date'), rcField('preInicioHora', 'Hora de inicio', 'time'), rcField('preFinFecha', 'Finalización', 'date'), rcField('preFinHora', 'Hora de finalización', 'time')];
27192    const cabanaRows = (group) => (rc[group] || []).map((item) => ({ id: item.id, name: item.nombre || '', logo: item.logoDataUrl || '', hasLogo: !!item.logoDataUrl, noLogo: !item.logoDataUrl, fileLabel: item.logoName || 'Seleccionar logo PNG', onName: (e) => this.setCabanaConfigEntry(group, item.id, { nombre: e.target.value }), onFile: (e) => this.readCabanaConfigLogo(group, item.id, e.target.files?.[0]), remove: () => this.removeCabanaConfigEntry(group, item.id) }));
27193    const cabanaParticipantGroups = [
27194      { title: 'Cabañas principales', help: 'Organizadoras y protagonistas del remate.', rows: cabanaRows('cabanasPrincipales'), add: () => this.addCabanaConfigEntry('cabanasPrincipales') },
27195      { title: 'Cabañas invitadas', help: 'Participantes adicionales del catálogo.', rows: cabanaRows('cabanasInvitadas'), add: () => this.addCabanaConfigEntry('cabanasInvitadas') },
27196    ];
27197    const hasRemateCatalog = !!rc.catalogoPdfName;
27198    const remateCatalogSizeMb = Number(rc.catalogoPdfSize || 0) / (1024 * 1024);
27199    const remateFieldTypeOptions = [rcOption('texto','Texto'), rcOption('numero','Número'), rcOption('fecha','Fecha'), rcOption('seleccion','Selección'), rcOption('booleano','Sí / No')];
27200    const remateCategoryForms = (rc.formulariosCategoria || []).map((rawCategory, c
27200ategoryIndex) => {
27201      const category = this.normalizeRemateCategoryForm(rawCategory);
27202      const visibleCount = category.formularios.flatMap((subform) => subform.campos || []).filter((field) => field.mostrarTarjeta).length;
27203      return {
27204        id: category.id,
27205        number: categoryIndex + 1,
27206        category: category.categoria || '',
27207        visibleLabel: visibleCount + ' / 4 en tarjeta',
27208        onCategory: (e) => this.setRemateCategoryForm(category.id, e.target.value),
27209        addSubform: () => this.addRemateSubform(category.id),
27210        remove: () => this.removeRemateCategoryForm(category.id),
27211        forms: category.formularios.map((subform, subformIndex) => ({
27212          id: subform.id,
27213          categoryId: category.id,
27214          dragLabel: `Ordenar subformulario ${subformIndex + 1}~${category.id}~${subform.id}`,
27215          dropLabel: `Destino subformulario~${category.id}~${subform.id}`,
27216          number: subformIndex + 1,
27217          name: subform.nombre || '',
27218          order: subform.orden || subformIndex + 1,
27219          active: subform.activo !== false,
27220          activeLabel: subform.activo !== false ?'Activo' : 'Inactivo',
27221          activeTrack: subform.activo !== false ?'#1f8a70' : '#cfd8e3',
27222          activeKnob: subform.activo !== false ?'16px' : '0px',
27223          onName: (e) => this.setRemateSubform(category.id, subform.id, { nombre: e.target.value }),
27224          onDragStart: (e) => this.startRemateConfigDrag('subform', category.id, subform.id, null, e),
27225          onDragEnter: (e) => this.reorderRemateSubforms(category.id, subform.id, e, true),
27226          onMouseDown: (e) => this.startRemateConfigDrag('subform', category.id, subform.id, null, e),
27227          onPointerDown: (e) => this.startRemateConfigDrag('subform', category.id, subform.id, null, e),
27228          onMouseEnter: (e) => { if (e.buttons === 1 && this._remateConfigDrag?.type === 'subform') this.reorderRemateSubforms(category.id, subform.id, e, true); },
27229          onPointerEnter: (e) => { if (this._remateConfigDrag?.type === 'subform') this.reorderRemateSubforms(category.id, subform.id, e, true); },
27230          onMouseUp: () => this.finishRemateConfigDrag(),
27231          onDragOver: (e) => this.allowRemateConfigDrop(e),
27232          onDrop: (e) => this.reorderRemateSubforms(category.id, subform.id, e),
27233          toggleActive: () => this.setRemateSubform(category.id, subform.id, { activo: subform.activo === false }),
27234          addField: () => this.addRemateFormField(category.id, subform.id),
27235          remove: () => this.removeRemateSubform(category.id, subform.id),
27236          fields: (subform.campos || []).map((field, fieldIndex) => ({
27237            id: field.id,
27238            categoryId: category.id,
27239            subformId: subform.id,
27240            dragLabel: `Ordenar campo ${fieldIndex + 1}~${category.id}~${subform.id}~${field.id}`,
27241            dropLabel: `Destino campo~${category.id}~${subform.id}~${field.id}`,
27242            number: fieldIndex + 1,
27243            name: field.nombre || '',
27244            type: field.tipo || 'texto',
27245            order: field.orden || fieldIndex + 1,
27246            optionsText: field.opciones || '',
27247            isSelection: field.tipo === 'seleccion',
27248            onName: (e) => this.setRemateFormField(category.id, subform.id, field.id, { nombre: e.target.value }),
27249            onType: (e) => this.setRemateFormField(category.id, subform.id, field.id, { tipo: e.target.value }),
27250            onOptions: (e) => this.setRemateFormField(category.id, subform.id, field.id, { opciones: e.target.value }),
27251            onDragStart: (e) => this.startRemateConfigDrag('field', category.id, subform.id, field.id, e),
27252            onDragEnter: (e) => this.reorderRemateFormFields(category.id, subform.id, field.id, e, true),
27253            onMouseDown: (e) => this.startRemateConfigDrag('field', category.id, subform.id, field.id, e),
27254            onPointerDown: (e) => this.startRemateConfigDrag('field', category.id, subform.id, field.id, e),
27255            onMouseEnter: (e) => { if (e.buttons === 1 && this._remateConfigDrag?.type === 'field') this.reorderRemateFormFields(category.id, subform.id, field.id, e, true); },
27256            onPointerEnter: (e) => { if (this._remateConfigDrag?.type === 'field') this.reorderRemateFormFields(category.id, subform.id, field.id, e, true); },
27257            onMouseUp: () => this.finishRemateConfigDrag(),
27258            onDragOver: (e) => this.allowRemateConfigDrop(e),
27259            onDrop: (e) => this.reorderRemateFormFields(category.id, subform.id, field.id, e),
27260            typeOptions: remateFieldTypeOptions,
27261            remove: () => this.removeRemateFormField(category.id, subform.id, field.id),
27262            characteristic: !!field.identificadorCaracteristico,
27263            characteristicLabel: field.identificadorCaracteristico ?'Sí':'No',
27264            toggleCharacteristic: () => this.setRemateFormField(category.id, subform.id, field.id, {identificadorCaracteristico: !field.identificadorCaracteristico}),
27265            toggleIdentifier: () => this.setRemateFormField(category.id, subform.id, field.id, { identificadorPrincipal: !field.identificadorPrincipal }),
27266            identifierTrack: field.identificadorPrincipal ?'#0a5ca8' : '#f7f9fc',
27267            identifierBorder: field.identificadorPrincipal ?'#0a5ca8' : '#dbe4ee',
27268            identifierText: field.identificadorPrincipal ?'#fff' : '#526276',
27269            identifierIconFilter: field.identificadorPrincipal ?'brightness(0) invert(1)' : 'none',
27270            toggleCard: () => this.setRemateFormField(category.id, subform.id, field.id, { mostrarTarjeta: !field.mostrarTarjeta }),
27271            cardTrack: field.mostrarTarjeta ?'#1f7a63' : '#f7f9fc',
27272            cardBorder: field.mostrarTarjeta ?'#1f7a63' : '#dbe4ee',
27273            cardText: field.mostrarTarjeta ?'#fff' : '#64748b',
27274            cardIconFilter: field.mostrarTarjeta ?'brightness(0) invert(1)' : 'none',
27275            cardOpacity: !field.mostrarTarjeta && visibleCount >= 4 ?'.5' : '1',
27276          })),
27277        })),
27278      };
27279    });
27280    const avatarCompany=s.user ? (s.userConsignataria || s.userConsignatarias?.[0]) : null;
27281    const userConsignatariaRef = avatarCompany?.id || avatarCompany?.slug || avatarCompany?.ref || avatarCompany?.name || '';
27282    const userConsignatariaMatch = userConsignatariaRef && (s.consignatarias || []).find((consignataria) => [consignataria.id, consignataria.slug, consignataria.name, consignataria.nombre].some((value) => this.consKey(value) === this.consKey(userConsignatariaRef)));
27283    const currentProfileData = this.profileApiData();
27284    const currentProfileName = [currentProfileData.firstName || currentProfileData.nombre, currentProfileData.lastName  || currentProfileData.apellido].filter(
27284Boolean).join(' ').trim();
27285    const advanceNameRaw = currentProfileData.firstName || currentProfileData.nombre || String(s.user?.displayName || '').trim().split(/\s+/)[0] || String(s.user?.email || '').split('@')[0];
27286    const advanceName = String(advanceNameRaw || '').trim();
27287    const advanceGreeting = advanceName ?'¡Hola, ' + advanceName.charAt(0).toUpperCase() + advanceName.slice(1) + '!' : '¡Hola!';
27288    const launchInterestSubscribed = s.launchInterest?.status === 'subscribed';
27289    const currentProfilePhoto = s.profilePhotoFailed ?'' : this.profilePhotoUrl(currentProfileData);
27290    const currentAvatar = this.commercialAvatar(currentProfileData, userConsignatariaRef, s.profilePhotoFailed);
27291    const userConsignatariaAvatar = currentAvatar.avatar;
27292    const userHasConsignatariaAvatar = !!userConsignatariaAvatar;
27293    const userHasProfilePhoto = !!currentProfilePhoto;
27294    const rgf = this.rosganForm();
27295    const rgCertifierOptions=[{uid:'',displayName:'Sin responsable comercial'},...(s.rosganCertifiers||[])].map(c=>{
27296      const own=String(c.uid)===String(s.user?.uid);
27297      const avatar=c.uid?this.commercialAvatar(own?currentProfileData:c,c.consignatariaId||rgf.consignatariaId,!!s.rosganCertifierPhotoFailed?.[c.uid]||(own&&s.profilePhotoFailed)):{avatar:'',hasAvatar:false,noAvatar:true,bg:'#edf3f8',size:'22px',fit:'contain'};
27298      const label=c.displayName||[c.firstName,c.lastName].filter(Boolean).join(' ')||c.email||c.uid;
27299      const selected=String(c.uid)===String(rgf.certifiedBy||'');
27300      return {...avatar,value:c.uid,label,initial:c.uid?label.charAt(0):'—',selected,rowBg:selected?'#edf4fa':'#fff',check:selected?'✓':'',avatarError:()=>{if(avatar.photo&&!this.state.rosganCertifierPhotoFailed?.[c.uid])this.setState({rosganCertifierPhotoFailed:{...this.state.rosganCertifierPhotoFailed,[c.uid]:true}});},pick:()=>this.setState({rosganLotForm:{...this.rosganForm(),certifiedBy:c.uid||null},rosganCertifierMenuOpen:false})};
27301    });
27302    const rgCategory = this.rosganSelectedCategory();
27303    const rgFormType = String(rgCategory?.formType || '').toLowerCase();
27304    const rgLotType = String(rgf.formType || rgCategory?.formType || '').toLowerCase();
27305    const rgFilteredCategories = (s.rosganLotCategories || []).filter((item) => String(item?.formType || '').toLowerCase() === rgLotType);
27306    const rgQuantityMode = String(rgCategory?.quantityMode || 'total').toLowerCase();
27307    const rgInput = (key) => (event) => {
27308      const tag = String(event.currentTarget?.tagName || '').toLowerCase();
27309      const type = String(event.currentTarget?.type || 'text').toLowerCase();
27310      const buffered = tag === 'textarea' || (tag === 'input' && !['checkbox', 'radio', 'range', 'file', 'date', 'time'].includes(type));
27311      this.setRosganLotField(key, event.target.value, buffered);
27312    };
27313    const rgToggle = (key) => () => this.setRosganLotField(key, !this.rosganForm()[key]);
27314    const rgOption = (...aliases) => this.rosganFieldOptions(...aliases);
27315    const rgDescriptionOptions = this.rosganDescriptionChoices();
27316    const rgSelectedDescriptionOptions = Array.isArray(rgf.descriptionOptionValues) ?rgf.descriptionOptionValues : [];
27317    const rgAgeUnitOptions = rgOption('ageUnits', 'ageUnit', 'unidadEdad', 'unidadDeEdad');
27318    const rgDentitionOptions = rgOption('dentitions', 'dentition', 'denticion');
27319    const rgReproductiveStatusOptions = rgFormType === 'vientres'
27320      ?rgOption('vientresReproductiveStatuses', 'reproductiveStatuses', 'reproductiveStatus', 'estadoReproductivo')
27321      :rgOption('invernadaReproductiveStatuses', 'reproductiveStatuses', 'reproductiveStatus', 'estadoReproductivo');
27322    const rgDestinationOptions = rgFormType === 'faena'
27323      ?rgOption('faenaDestinations', 'destinations', 'destination', 'destino')
27324      :rgOption('generalDestinations', 'destinations', 'destination', 'destino');
27325    const rgBodyConditionOptions = rgOption('bodyConditions', 'bodyCondition', 'condicionCorporal');
27326    const rgAnimalClassOptions = rgOption('classes', 'animalClasses', 'animalClass', 'claseAnimal', 'clase');
27327    const rgTickStatusOptions = rgOption('tickStatuses', 'tickStatus', 'estadoGarrapata', 'garrapata');
27328    const rgHealthOptions = rgOption('healthStatuses', 'healthStatus', 'health', 'sanidad');
27329    const rgNutritionOptions = rgOption('nutritions', 'nutrition', 'nutricion');
27330    const rgWeighingOptions = rgOption('weighings', 'weighing', 'pesaje');
27331    const rgServiceOptions = rgOption('service', 'tipoServicio', 'servicio');
27332    const rgPregnancyOptions = rgOption('pregnancy', 'prenez', 'preñez');
27333    const rgCalvingOptions = rgOption('calving', 'paricion');
27334    const rgSubCategoryOptions = this.rosganFaenaSubCategoryChoices();
27335    const rgConditionTypeOptions = rgOption('faenaConditionTypes', 'conditionTypes', 'conditionType', 'condicion');
27336    const rgDeliveryTypeOptions = rgOption('faenaDeliveryTypes', 'deliveryTypes', 'deliveryType', 'entrega');
27337    const rgFinishingOptions = rgOption('faenaFinishings', 'finishings', 'finishing', 'terminacion');
27338    const rgDate = (value) => { try { return new Date(value).toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return ''; } };
27339    const rgStep = Number(s.rosganLotStep || 0);
27340    const rgRemate = (s.rosganLotRemates || []).find((item) => String(item.id) === String(rgf.remateId));
27341    const rgConsignataria = (s.rosganLotConsignatarias || []).find((item) => String(item.id) === String(rgf.consignatariaId));
27342    const rgConsignatariaOptions = this.rosganConsignatariasForRemate().map((item) => {
27343      const id = String(item.id);
27344      const name = item.name || item.nombre || item.consignatariaNombre || item.id;
27345      const logo = this.consLogo(name) || item.logoUrl || item.logo || item.imageUrl;
27346      const initials = String(name || '').split(/\s+/).filter(Boolean).slice(0, 2).map((word) => word.charAt(0).toUpperCase()).join('') || 'C';
27347      const selected = id === String(rgf.consignatariaId);
27348      return {
27349        id, name, logo, initials, selected, hasLogo: !!logo, showsInitials: !logo,
27350        className: 'rg-cons-picker-option' + (selected ?' is-selected' : ''),
27351        pick: () => this.onRosganConsignatariaChange(id),
27352      };
27353    });
27354    const rgSelectedConsignataria = rgConsignatariaOptions.find((item) => item.selected) || null;
27355    const rgRelationScope = this.rosganLotRelationScope();
27356    const rgRelationsReady = !!rgRelationScope && s.rosganLotRelationsScope === rgRelationScope && !s.rosganLotRelationsLoading;
27357    const rgScopedComitentes = rgRelationsReady ?(s.rosganLotComitentes || []) : [];
27358    const rgComitenteOptions = rgScopedComitentes;
27359    const rgComitente = rgComitenteOptions.find((item) => String(item.id) === String(rgf.comitenteId));
27360    const rgComitenteSearch = rgRelationsReady ?(s.rosganComitenteSearch || (rgComitente ?String(rgComitente.alias || rgComitente.name || rgComitente.nombre || rgComitente.nombreFiscal || rgComitente.fiscalName || '') : '')) : '';
27361    const rgComitenteQuery = this.consKey(rgComitenteSearch);
27362    const rgComitenteMatches = rgComitenteOptions.filter((item) => {
27363      if (!rgComitenteQuery) return true;
27364      return this.consKey([item.alias, item.name, item.nombre, item.nombreFiscal, item.fiscalName, item.razonSocial, item.cuitCuil, item.cuit, item.cuitNormalized, item.locality, item.localidad].filter(Boolean).join(' ')).includes(rgComitenteQuery);
27365    }).sort((a,b) => String(a.alias || a.name || '').localeCompare(String(b.alias || b.name || ''), 'es'));
27366    const rgFilteredComitentes = rgComitenteMatches.slice(0, s.rosganComitenteLimit || 60).map((item) => {
27367      const id = String(item.id || item.comitenteId || '');
27368      const name = item.alias || item.name || item.nombre || item.nombreFiscal || item.fiscalName || item.razonSocial || id;
27369      const meta = [item.cuitCuil || item.cuit || item.cuitNormalized, item.fiscalName || item.nombreFiscal || item.razonSocial].filter(Boolean).join(' · ');
27370      const selected = id === String(rgf.comitenteId);
27371      return { id, name, meta, hasMeta: !!meta, selected, bg: selected ?'#eaf4ff' : '#fff', icon:item.validated ? 'dc-lucide dc-lucide-circle-check' : 'dc-lucide dc-lucide-triangle-alert', color:item.validated ? '#14805e' : '#b56b08', status:item.validated ? 'CUIT validado' : 'Pendiente de validar CUIT', pick: () => this.selectRosganComitente(item) };
27372    });
27373    const rgComitenteForm = s.rosganComitenteForm || { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' };
27374    const rgComitenteLocalityOptions = (s.geoLocs?.[rgComitenteForm.province] || []).map((locality) => locality?.nombre || locality?.name || locality).filter(Boolean);
27375    if (rgComitenteForm.locality && !rgComitenteLocalityOptions.some((item) => this.consKey(item) === this.consKey(rgComitenteForm.locality))) rgComitenteLocalityOptions.unshift(rgComitenteForm.locality);
27376    const rgComitenteFiscalLocked = !rgComitenteForm.source || !!s.rosganComitenteLookupLoading;
27377    const rgComitenteReady = !!String(rgComitenteForm.alias || '').trim() && !s.rosganComitenteLookupLoading;
27378    const rgComitenteLocalityDisabled = rgComitenteFiscalLocked || !rgComitenteForm.province || !!s.geoLoadingLoc;
27379    const rgAvailableEstablishments = (rgRelationsReady && rgf.comitenteId ?(s.rosganLotEstablishments || []) : []).filter((item) => {
27380      return (!item.consignatariaId || String(item.consignatariaId) === String(rgf.consignatariaId)) && item.isActive !== false && String(item.comitenteId || '') === this.rosganSelectedOperationComitenteId();
27381    });
27382    const rgEstablishment = rgAvailableEstablishments.find((item) => String(item.id || item.establishmentId || item.establecimientoId) === String(rgf.establishmentId));
27383    const rgEstablishmentQuery = this.consKey(s.rosganEstablishmentSearch || '');
27384    const rgEstablishmentMatches = rgAvailableEstablishments.filter(item => !rgEstablishmentQuery || this.consKey([item.name, item.locality, item.province].filter(
27384Boolean).join(' ')).includes(rgEstablishmentQuery)).sort((a,b) => String(a.name || '').localeCompare(String(b.name || ''), 'es'));
27385    const rgEstablishmentOptions = rgEstablishmentMatches.slice(0, s.rosganEstablishmentLimit || 60).map(item => ({
27386      id:String(item.id), name:item.name || item.id, selected:String(item.id) === String(rgf.establishmentId),
27387      color:item.validated ? '#14805e' : '#b56b08', background:item.validated ? '#e6f5ed' : '#fff2d8',
27388      icon:item.validated ? 'dc-lucide dc-lucide-circle-check' : 'dc-lucide dc-lucide-triangle-alert', status:item.validated ? 'CUIT validado' : 'Pendiente de validar CUIT',
27389      meta:[item.locality, item.province].filter(Boolean).join(' · '), pick:() => this.selectRosganEstablishment(item.id),
27390    }));
27391    const rgEstablishmentForm = s.rosganEstablishmentForm || this.emptyRosganEstablishmentForm();
27392    const rgEstablishmentLocalities = (s.geoLocs?.[rgEstablishmentForm.provincia] || []).map((locality) => locality?.nombre || locality?.name || locality).filter(Boolean);
27393    const rgBreedFamilies = this.rosganBreedFamilies();
27394    const rgBreedIconMap = this.rosganBreedIconMap();
27395    const rgBreedQuery = this.consKey(s.rosganBreedSearch || '');
27396    const rgSelectedBreedFamilies = rgBreedFamilies.filter((group) => group.key !== '__crosses__' && group.items.some((item) => rgf.breedIds.includes(String(item.id))));
27397    const rgBreedCrosses = s.rosganBreedCrosses || {};
27398    const rgBreedNames = rgSelectedBreedFamilies.map((group) => {
27399      const selectedItems = group.items.filter((item) => rgf.breedIds.includes(String(item.id)));
27400      const blackSelected = (!!group.both && selectedItems.some((item) => String(item.id) === String(group.both.id))) || (!!group.black && selectedItems.some((item) => String(item.id) === String(group.black.id)));
27401      const redSelected = (!!group.both && selectedItems.some((item) => String(item.id) === String(group.both.id))) || (!!group.red && selectedItems.some((item) => String(item.id) === String(group.red.id)));
27402      let selectedName = selectedItems[0]?.name || group.label;
27403      if (group.supportsColors && blackSelected && redSelected) selectedName = group.both?.name || `${group.label} Negro y Colorado`;
27404      return selectedName + (rgBreedCrosses[group.key] ?' y sus cruzas' : '');
27405    });
27406    const rgActiveBreedFamily = rgBreedFamilies.find((group) => group.key === s.rosganActiveBreedFamily) || rgSelectedBreedFamilies.find((group) => group.supportsColors) || null;
27407    const rgBreedMatches = rgBreedQuery
27408      ?rgBreedFamilies.filter((group) => group.key !== '__crosses__' && this.consKey([group.label, ...group.items.map((item) => item.name)].join(' ')).includes(rgBreedQuery))
27409      : [];
27410    const rgBreedSearchFamilies = rgBreedMatches.slice(0, 16);
27411    const rgBreedFamilyView = (group) => {
27412      const ids = group.items.map((item) => String(item.id));
27413      const selected = ids.some((id) => rgf.breedIds.includes(id));
27414      const blackSelected = (!!group.both && rgf.breedIds.includes(String(group.both.id))) || (!!group.black && rgf.breedIds.includes(String(group.black.id)));
27415      const redSelected = (!!group.both && rgf.breedIds.includes(String(group.both.id))) || (!!group.red && rgf.breedIds.includes(String(group.red.id)));
27416      return { ...group, selected, blackSelected, redSelected };
27417    };
27418    const rgActiveBreedView = rgActiveBreedFamily ?rgBreedFamilyView(rgActiveBreedFamily) : null;
27419    const rgHeads = rgQuantityMode === 'male_female' ?(Number(rgf.maleQuantity) || 0) + (Number(rgf.femaleQuantity) || 0) : rgQuantityMode === 'mothers_calves' ?(Number(rgf.motherQuantity) || 0) + (Number(rgf.calfQuantity) || 0) : (Number(rgf.quantity) || 0);
27420    const rgCompleteGroups = [this.rosganLotStepComplete(0), this.rosganLotStepComplete(1), this.rosganLotStepComplete(2)];
27421    const rgCompleteness = Math.round((rgCompleteGroups.filter(Boolean).length / rgCompleteGroups.length) * 100);
27422    const rgCreatedLot = s.rosganCreatedLot || null;
27423    const rgCreatedVideoKey = rgCreatedLot?.remateId && rgCreatedLot?.lotCert ?this.rosganLotVideoKey(rgCreatedLot.remateId, rgCreatedLot.lotCert) : '';
27424    const rgCreatedVideoState = rgCreatedVideoKey ?(s.rosganLotVideoState?.[rgCreatedVideoKey] || { loading: false, videos: [], uploads: [], error: '' }) : { loading: false, videos: [], uploads: [], error: '' };
27425    const rgCreatedVideoUploadsRaw = Array.isArray(rgCreatedVideoState.uploads) ?rgCreatedVideoState.uploads : [];
27426    const rgCreatedVideoBusy = !!rgCreatedVideoState.uploading;
27427    const rgCreatedHasVideos = !!(rgCreatedVideoState.videos || []).length;
27428    const rgPreviewBrandName = rgConsignataria?.name || rgConsignataria?.nombre || rgConsignataria?.consignatariaNombre || '';
27429    const rgPreviewLogo = rgPreviewBrandName ? (this.consWhiteLogo(rgPreviewBrandName) || this.consLogo(rgPreviewBrandName) || rgConsignataria?.logoUrl || '') : '';
27430    const rgPreviewThumbnail = this.rosganActiveLotThumbnail({ videos: rgCreatedVideoState.videos || [] });
27431    return {
27432      // La restauracion de una sesion existente es silenciosa: el usuario no
27433      // debe volver a ver una pantalla de acceso cada vez que recarga la app.
27434      isAuthRestoring: !!s.routeLoading || (!s.publicAdvanceMode && !s.authReady && !s.authWelcomeRequested),
27435      isWelcome: s.authReady && s.route === 'welcome',
27436      showConnectingScreen: !s.publicAdvanceMode && !s.authReady && !!s.authWelcomeRequested,
27437      showWelcomeScreen: !s.publicAdvanceMode && s.authReady && s.route === 'welcome',
27438      connectingLottieSrc: !s.publicAdvanceMode && !s.authReady && !!s.authWelcomeRequested ?'assets/auth/options/connecting-1.json?v=ceres-celeste-1' : '',
27439      welcomeLottieSrc: !s.publicAdvanceMode && s.authReady && s.route === 'welcome' ?'assets/auth/options/success-1.json' : '',
27440      authTransitioning: !!s.authTransitioning,
27441      advanceGreeting,
27442      showStatistics: s.route === 'mercado' && this.canAccessStatistics(s.apiUser),
27443      launchInterestSubscribed,
27444      launchInterestSaving: !!s.launchInterestSaving,
27445      launchInterestButtonDisabled: !!s.launchInterestLoading || !!s.lau
27445nchInterestSaving || launchInterestSubscribed,
27446      launchInterestButtonClass: 'advance-splash-launch' + (launchInterestSubscribed ?' is-subscribed' : ''),
27447      launchInterestButtonLabel: s.launchInterestLoading ?'Consultando…' : (s.launchInterestSaving ?'Guardando…' : (launchInterestSubscribed ?'¡Listo! Te avisaremos' : 'Quiero enterarme del lanzamiento')),
27448      launchInterestHasMessage: !!s.launchInterestMessage,
27449      launchInterestMessage: s.launchInterestMessage || '',
27450      toggleLaunchInterest: () => this.toggleLaunchInterest(),
27451      isLogin: s.authReady && !s.publicAdvanceMode && s.route === 'login' && !s.emailVerificationRequired && !s.phoneVerificationRequired,
27452      isEmailVerification: s.authReady && !s.publicAdvanceMode && !!s.user && !!s.emailVerificationRequired,
27453      isPhoneVerification: s.authReady && !s.publicAdvanceMode && !!s.user && !s.emailVerificationRequired && !!s.phoneVerificationRequired,
27454      isApp: !s.routeLoading && s.authReady && !s.emailVerificationRequired && !s.phoneVerificationRequired && !['login', 'welcome'].includes(s.route),
27455      isDevActivityPage: s.route === 'devActivity' && !!s.user && this.userRoleIds(s.apiUser).includes('dev'),
27456      isHome: s.route === 'home', isPool: s.route === 'pool', isRoslotSummary: s.route === 'roslotSummary', isLot: s.route === 'lot' && !!lot, isLoad: s.route === 'load', isRosganLotLoad: s.route === 'rosganLotLoad', isFeria: s.route === 'feria', isFeriaConfig: s.route === 'feriaConfig' && this.canManageUsers(s.apiUser), isProfile: !!s.user && s.route === 'profile', isProfileArea: !!s.user && ['profile', 'mailingAdmin'].includes(s.route), profileDashboardVisible: !!s.user && s.route === 'profile', profileActivityVisible: !!s.user && s.route === 'profile' && !this.isBasicUserProfile(s.apiUser), isMailingAdmin: s.route === 'mailingAdmin', isNotifications: s.route === 'notifications', isRemates: ['remates', 'rosganRemateCreate'].includes(s.route), isRematesIndex: s.route === 'remates', isRosganRemateCreate: s.route === 'rosganRemateCreate', isRemateDetail: s.route === 'remateDetail', isRosganSalesOrder: s.route === 'rosganSalesOrder' && !!s.currentRemate, isRosganStats:s.route==='rosganStats', isRosganResults:s.route==='rosganResults', resultsPageKey:s.resultsPageKey||'', resultsBackHref:s.resultsBackHref||'/remates', rdResultsHref:'/remates/'+encodeURIComponent(this.remateRouteId(s.currentRemate))+'/resultados'+(s.currentRemate?.__rosganActive?'':'?origen=historico'), rgAudienceId:this.remateRouteId(s.currentRemate), rdResultsKey:(s.currentRemate?.__rosganActive?'actual:':'historico:')+this.remateRouteId(s.currentRemate),
27457      isRosganCommercial: s.route === 'rosganCommercial' && !!s.currentRemate, isRosganPriceBoard: s.route === 'rosganPriceBoard' && !!s.currentRemate, isRosganTvLower: s.route === 'rosganTvLower' && !!s.currentRemate, isRosganTvConfig: s.route === 'rosganTvConfig' && !!s.currentRemate, isCabanas: s.route === 'cabanas' && !s.cabanaPublicDeepLoading, isCabanaPublicLoading: s.authReady && (!!s.cabanaBrandPreparing || (s.publicAdvanceMode === 'lacassina' && !!s.cabanaPublicDeepLoading)), isCabanaDetail: s.route === 'cabanaDetail' && !!s.currentCabana && !s.cabanaPublicDeepLoading && !s.cabanaBrandPreparing, isCabanaPrebidPanel: s.route === 'cabanaPrebidPanel' && !!s.currentCabana, isCabanaLotDetail: s.route === 'cabanaLotDetail' && !!s.currentCabanaLot && !s.cabanaPublicDeepLoading, isCabanaLotCreate: s.route === 'cabanaLotCreate' && !!s.cabanaLotCreateRemate, isCabanaStats: s.route === 'cabanaStats' && (!!s.cabanaStatsOverview || !!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/estadisticas')), isCabanaCommercial: s.route === 'cabanaCommercial' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/comercial')), isCabanaPriceBoard: s.route === 'cabanaPriceBoard' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/precios')), isCabanaTvLower: s.route === 'cabanaTvLower' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv')), isRemateConfig: s.route === 'remateConfig', isCabanaSalesOrder: s.route === 'cabanaSalesOrder' && !!s.currentCabana, isChannel: s.route === 'channel', isMercado: s.route === 'mercado',
27458      ...this.roslotDailyReportView(),
27459      ...this.mercadoData(),
27460      ...this.mavData(),
27461      ...this.mapaData(),
27462      isRosganAuditor: s.route === 'rosganAuditor',
27463      rgAuditorRouteBusy: !s.rosganAuditorRouteError,
27464      rgAuditorRouteError: !!s.rosganAuditorRouteError,
27465      rgAuditorRouteMessage: s.rosganAuditorRouteError || 'Cargando panel auditor…',
27466      retryRgAuditor: () => { const target = this.rematePathTarget(); if (target?.action === 'auditor') this.openRosganAuditorById(target.remateId); },
27467      exitRgAuditor: () => this.go('remates'),
27468      showLightBar: !s.publicAdvanceMode && ['pool', 'roslotSummary', 'lot', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'remateDetail', 'rosganAuditor', 'rosganSalesOrder', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'remateConfig', 'cabanaSalesOrder', 'channel', 'mercado'].includes(s.route),
27469      isMobile: s.isMobile, isDesktop: !s.isMobile, compactNavigation: s.compactNavigation, desktopNavigation: !s.compactNavigation,
27470      notificationsOpen: !s.publicAdvanceMode && !!s.notificationsOpen,
27471      notificationItems,
27472      notificationsLoading: !!s.notificationsLoading,
27473      notificationsHasError: !!s.notificationsError,
27474      notificationsError: s.notificationsError,
27475      notificationsEmpty: !s.notificationsLoading && !s.notificationsError && Array.isArray(s.notificationsItems) && notificationItems.length === 0,
27476      hasUnreadNotifications: unreadNotifications > 0,
27477      notificationsUnreadLabel: unreadNotifications > 99 ?'99+' : String(unreadNotifications),
27478      notificationBellTitle: unreadNotifications ?`${unreadNotifications} notificaciones sin leer` : 'Notificaciones',
27479      notificationBellBg: s.notificationsOpen ?'#dcecff' : '#fff',
27480      toggleNotifications: () => this.toggleNotifications(),
27481      markAllNotificationsRead: () => this.markAllNotificationsRead(),
27482      goNotificationCenter: () => this.goNotificationCenter(),
27483      notificationHistoryItems,
27484      notificationHistoryLoading: !!s.notificationHistoryLoading,
27485      notificationHistoryHasError: !!s.notificationHistoryError,
27486      notificationHistoryError: s.notificationHistoryError,
27487      notificationHistoryEmpty: !s.notificationHistoryLoading && !s.notificationHistoryError && Array.isArray(s.notificationHistory) && notificationHistoryItems.length === 0,
27488      canLoadLot,
27489      canLoadRosganLot: this.canManageRosganLots(s.apiUser),
27490      canCreateEstablishment: this.canManageComitentes(s.apiUser),
27491      manualEstLocked: true,
27492      goRosganLotLoad: () => this.go('rosganLotLoad'),
27493      toggleRematePanelPicker: () => this.showRematePanelPicker(),
27494      rgCanViewPanel: this.canManageRosganLots(),
27495      rgPanelLabel: this.isRosganProfile() ? 'Panel Consignatario' : 'Panel Auditor',
27496      rgPanelDisabled: !rgRemate,
27497      rgPanelOpacity: rgRemate ? 1 : 0.5,
27498      rgPanelHint: rgRemate ? 'Abrir el panel del remate seleccionado' : 'Seleccioná un remate para abrir su panel',
27499      openRgPanel: () => { if (this.canManageRosganLots() && rgRemate) this.openRosganAuditor(rgRemate); },
27500      rgWizardTitle: s.rosganEditingLot ?'Editar lote Rosgan · ' + s.rosganEditingLot.lotCert : 'Cargar lote Rosgan', rgWizardSubtitle: 'Remates generales · Invernada, Vientres y Faena',
27501      rgEditingLot: !!s.rosganEditingLot,
27502      rgStepKey: 'rg-step-' + rgStep, rgIsStep0: rgStep === 0, rgIsStep1: rgStep === 1, rgIsStep2: rgStep === 2, rgIsStep3: rgStep === 3,
27503      rgWizardSteps: [
27504        { title: 'Procedencia', text: 'Remate y origen' }, { title: 'Categoría', text: 'Raza y cantidad' }, { title: 'Detalles', text: 'Datos del lote' }, { title: 'Videos', text: 'Opcionales' },
27505      ].map((item, index) => ({ ...item, go: () => {
27506        const canGo = rgCreatedLot ?index === 3 : (index <= rgStep || (index === rgStep + 1 && index < 3 && this.rosganLotStepComplete(rgStep)));
27507        if (canGo) this.setState({ rosganLotStep: index, rosganLotMessage: null });
27508      }, barBg: index <= rgStep ?'#0a5ca8' : '#dce6f0', dotBg: index < rgStep ?'#1f7a63' : index === rgStep ?'#0a5ca8' : '#fff', dotColor: index <= rgStep ?'#fff' : '#8b9bad', dotBorder: index <= rgStep ?
27508(index < rgStep ?'#1f7a63' : '#0a5ca8') : '#d5e0eb', done: index < rgStep, numberVisible: index >= rgStep, number: String(index + 1), titleColor: index === rgStep ?'#112033' : '#708196' })),
27509      rgPrevVisible: rgStep > 0 && rgStep < 3 ?'visible' : 'hidden', rgPrevStep: () => { if (rgStep < 3) this.setState({ rosganLotStep: Math.max(0, rgStep - 1), rosganLotMessage: null }); }, rgNextStep: () => this.rosganLotNext(),
27510      rgNextLabel: rgStep === 2 ?(s.rosganLotPublishing ?'Guardando lote…' : s.rosganEditingLot ?'Guardar cambios' : 'Cargar lote') : rgStep === 3 ?(rgCreatedVideoBusy ?'Subiendo videos…' : (rgCreatedHasVideos ?'Finalizar carga' : 'Finalizar sin videos')) : 'Continuar',
27511      rgNextShowsArrow: rgStep < 2 && !s.rosganLotPublishing, rgNextBg: rgStep === 2 ?'#e63946' : rgStep === 3 ?'#1f7a63' : '#0a5ca8', rgNextDisabled: !!s.rosganLotPublishing || rgCreatedVideoBusy,
27512      rgCreatedVideoBusy, rgCreatedLotCert: rgCreatedLot?.lotCert || 'no informado', rgCreatedLotCanUpload: !!rgCreatedVideoKey, rgCreatedLotCannotUpload: !!rgCreatedLot && !rgCreatedVideoKey,
27513      uploadRgCreatedLotVideos: (event) => { const files = event.target.files; this.uploadRosganLotVideos(files, rgCreatedLot); event.target.value = ''; },
27514      prepareCreatedFieldLot: () => this.prepareFieldLot(rgCreatedLot),
27515      rgCreatedVideoLoading: !!rgCreatedVideoState.loading,
27516      rgCreatedVideoHasError: !!rgCreatedVideoState.error, rgCreatedVideoError: rgCreatedVideoState.error || '',
27517      rgCreatedVideoHasUploads: !!rgCreatedVideoUploadsRaw.length,
27518      rgCreatedVideoUploads: rgCreatedVideoUploadsRaw.map((upload) => ({
27519        name: upload.name || 'Video', progress: Math.max(0, Math.min(100, Number(upload.progress) || 0)), size: this.formatFileSize(upload.sizeBytes),
27520        status: upload.status || 'En espera', color: upload.status === 'Error' ?'#b42332' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8',
27521        bar: upload.status === 'Error' ?'#d94a58' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8',
27522        error: upload.error || '', hasError: !!upload.error, canRetry: !!upload.canRetry, retryLabel: upload.retryLabel, retry: () => this.retryRosganVideoUpload(upload.id, rgCreatedLot),
27523      })),
27524      rgCreatedVideosHasItems: !rgCreatedVideoState.loading && !!(rgCreatedVideoState.videos || []).length,
27525      rgCreatedVideosEmpty: !rgCreatedVideoState.loading && !rgCreatedHasVideos && !rgCreatedVideoBusy,
27526      rgCreatedVideos: (rgCreatedVideoState.videos || []).map((video, index) => {
27527        const id = String(video?.id || video?.videoId || video?.videoID || '');
27528        const url = video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl || '';
27529        const name = video?.fileName || video?.name || video?.title || ('Video ' + (index + 1));
27530        return { id, url, hasUrl: !!url, noUrl: !url, name, size: this.formatFileSize(video?.sizeBytes || video?.fileSize || video?.size), deleting: s.rosganLotVideoDeleting === rgCreatedVideoKey + ':' + id, remove: () => this.deleteRosganLotVideo(video, rgCreatedLot) };
27531      }),
27532      rgLayoutCols: s.isMobile ?'minmax(0,1fr)' : 'minmax(0,1fr) 320px', rgShowPreview: !s.isMobile,
27533      rgPreviewCategory: rgCategory ?rgCategory.name + (rgf.isTermino ?' a Término' : '') : 'Categoría pendiente', rgPreviewType: rgLotType ?rgLotType.charAt(0).toUpperCase() + rgLotType.slice(1) : 'Tipo de lote',
27534      rgPreviewRemate: rgRemate?.titulo || rgRemate?.nombre || 'Seleccioná un remate', rgPreviewConsignataria: rgConsignataria?.name || rgConsignataria?.nombre || 'Consignataria pendiente',
27535      rgPreviewBrandColor: rgPreviewBrandName ? this.consBrandColor(rgPreviewBrandName) : '#002e5f',
27536      rgPreviewLogo, rgPreviewHasLogo: !!rgPreviewLogo,
27537      rgPreviewThumbnail, rgPreviewHasThumbnail: !!rgPreviewThumbnail,
27538      rgPreviewImageError: (event) => { event.currentTarget.style.display = 'none'; },
27539      rgPreviewImageLoaded: (event) => { event.currentTarget.style.display = 'block'; },
27540      rgPreviewComitente: rgComitente?.alias || rgComitente?.name || rgComitente?.nombre || rgComitente?.nombreFiscal || rgComitente?.fiscalName || 'Comitente pendiente', rgPreviewEstablishment: rgEstablishment?.name || rgEstablishment?.nombre || 'Establecimiento pendiente',
27541      rgPreviewHeads: rgHeads ?String(rgHeads) : '—', rgPreviewBreeds: rgBreedNames.length ?rgBreedNames.join(' · ') : 'Raza pendiente', rgPreviewWeight: rgf.averageWeight ?rgf.averageWeight + ' kg' : '—',
27542      rgPreviewCompleteness: rgCompleteness + '%', rgPreviewProgress: rgCompleteness + '%',
27543      rosganLotLoading: !!s.rosganLotLoading,
27544      rosganLotReady: !s.rosganLotLoading && Array.isArray(s.rosganLotRemates) && Array.isArray(s.rosganLotCategories),
27545      rosganLotMessageVisible: !!s.rosganLotMessage,
27546      rosganLotMessage: s.rosganLotMessage?.text || '',
27547      rosganLotMessageBg: s.rosganLotMessage?.ok ?'#e7f5ef' : '#fff0f1',
27548      rosganLotMessageColor: s.rosganLotMessage?.ok ?'#176c58' : '#b42335',
27549      rosganLotRemates: (s.rosganLotRemates || []).map((item) => ({ id: String(item.id), name: item.titulo || item.nombre || item.id, meta: [rgDate(item.fechaHoraRemate), item.localidad].filter(
27549Boolean).join(' · ') })),
27550      rosganLotConsignatarias: rgConsignatariaOptions,
27551      rosganLotComitentes: rgScopedComitentes.map((item) => ({ id: String(item.id), name: item.alias || item.name || item.nombre || item.nombreFiscal || item.fiscalName || item.razonSocial || item.id })),
27552      rosganLotEstablishments: rgAvailableEstablishments.map((item) => ({ id: String(item.id || item.establishmentId || item.establecimientoId), name: item.name || item.nombre || item.id, meta: [item.locality || item.localidad, item.province || item.provincia].filter(Boolean).join(' · ') })),
27553      rgEstablishmentOptions,
27554      rgEstablishmentMenuOpen: !!s.rosganEstablishmentMenuOpen && rgRelationsReady && !!rgf.comitenteId,
27555      rgEstablishmentSearch: s.rosganEstablishmentSearch || '',
27556      rgHasSelectedEstablishment: !!rgEstablishment,
27557      rgEstablishmentButtonLabel: !rgf.comitenteId ?'Elegí un comitente primero' : rgEstablishment?.name || 'Seleccionar establecimiento…',
27558      rgEstablishmentStatusColor: rgEstablishment?.validated ? '#14805e' : '#b56b08',
27559      rgEstablishmentStatusBg: rgEstablishment?.validated ? '#e6f5ed' : '#fff2d8',
27560      rgEstablishmentStatusIcon: rgEstablishment?.validated ? 'dc-lucide dc-lucide-circle-check' : 'dc-lucide dc-lucide-triangle-alert',
27561      rgEstablishmentNoMatches: !rgEstablishmentMatches.length,
27562      rgEstablishmentHasMore: rgEstablishmentOptions.length < rgEstablishmentMatches.length,
27563      rgEstablishmentCountLabel: rgEstablishmentOptions.length + ' de ' + rgEstablishmentMatches.length + ' establecimientos',
27564      toggleRgEstablishmentMenu: () => rgf.comitenteId && rgRelationsReady && this.setState({ rosganEstablishmentMenuOpen:!s.rosganEstablishmentMenuOpen, rosganEstablishmentSearch:'', rosganEstablishmentLimit:60 }),
27565      onRgEstablishmentSearch: event => this.bufferInputState('rosgan-establishment-search', { rosganEstablishmentSearch:event.target.value, rosganEstablishmentLimit:60 }, 100),
27566      moreRgEstablishments: () => this.setState({ rosganEstablishmentLimit:(s.rosganEstablishmentLimit || 60) + 60 }),
27567      blurRgEstablishmentPicker: () => setTimeout(() => { if (!document.activeElement?.closest('.rg-est-picker')) this.setState({ rosganEstablishmentMenuOpen:false }); }, 150),
27568      onRgEstablishmentPickerKey: event => {
27569        if (!rgf.comitenteId || !rgRelationsReady) return;
27570        if (event.key === 'Escape') { event.preventDefault(); this.setState({ rosganEstablishmentMenuOpen:false }); document.querySelector('.rg-est-trigger')?.focus(); }
27571        else if (['ArrowDown','ArrowUp'].includes(event.key)) { event.preventDefault(); const move=()=>{ const choices=[...document.querySelectorAll('[data-rg-est-choice]')]; const current=choices.indexOf(document.activeElement); choices[current < 0 ? (event.key === 'ArrowDown' ? 0 : choices.length - 1) : (current + (event.key === 'ArrowDown' ? 1 : -1) + choices.length) % choices.length]?.focus(); }; if (!s.rosganEstablishmentMenuOpen) this.setState({ rosganEstablishmentMenuOpen:true },move); else move(); }
27572      },
27573      rosganLotCategories: rgFilteredCategories.map((item) => ({ id: String(item.id), name: item.name })),
27574      rgLotTypeOptions: [
27575        { value: 'invernada', label: 'Invernada' },
27576        { value: 'faena', label: 'Faena' },
27577        { value: 'vientres', label: 'Vientres' },
27578      ].map((item) => {
27579        const selected = rgLotType === item.value;
27580        return { ...item, selected: selected ?'true' : 'false', className: selected ?'is-selected' : '', bg: selected ?'#0a5ca8' : '#f5f8fb', color: selected ?'#fff' : '#40566d', border: selected ?'#0a5ca8' : '#ccdae8', pick: () => this.selectRosganLotType(item.value) };
27581      }),
27582      rgCategoryDisabled: !rgLotType,
27583      rgCategoryBg: rgLotType ?'#fff' : '#eef2f6',
27584      rgCategoryPlaceholder: rgLotType ?'Seleccionar categoría…' : 'Elegí primero Invernada, Faena o Vientres',
27585      rosganLotBreeds: (s.rosganLotBreeds || []).map((item) => ({ id: String(item.id), name: item.name })),
27586      rosganLotOptionsLoading: !!s.rosganLotOptionsLoading,
27587      rgDescriptionOptions: rgDescriptionOptions.map((option) => {
27588        const selected = rgSelectedDescriptionOptions.some((item) => [option.value,option.label].some(v => this.consKey(item) === this.consKey(v)));
27589        return { ...option, selected: selected ?'true' : 'false', bg: selected ?'#0a5ca8' : '#f5f8fb', 
27589color: selected ?'#fff' : '#52667b', border: selected ?'#0a5ca8' : '#c9daea', shadow: selected ?'0 6px 14px rgba(10,92,168,.18)' : 'none', markBg: selected ?'rgba(255,255,255,.2)' : '#e1e8ef', markColor: selected ?'#fff' : '#718397', mark: selected ?'✓' : '+', pick: () => this.toggleRosganLotDescription(option.value) };
27590      }),
27591      rgHasDescriptionOptions: !!rgDescriptionOptions.length,
27592      rgAgeUnitOptions: rgAgeUnitOptions.length ?rgAgeUnitOptions : [{ value: 'meses', label: 'Meses' }, { value: 'años', label: 'Años' }],
27593      rgDentitionOptions, rgHasDentitionOptions: !!rgDentitionOptions.length, rgHasNoDentitionOptions: !rgDentitionOptions.length,
27594      rgReproductiveStatusOptions, rgHasReproductiveStatusOptions: !!rgReproductiveStatusOptions.length, rgHasNoReproductiveStatusOptions: !rgReproductiveStatusOptions.length,
27595      rgDestinationOptions, rgHasDestinationOptions: !!rgDestinationOptions.length, rgHasNoDestinationOptions: !rgDestinationOptions.length,
27596      rgDestinationChoices: rgDestinationOptions.map((o) => ({ ...o, selected: String(rgf.destination) === o.value ? 'true' : 'false', border: String(rgf.destination) === o.value ? '#0a5ca8' : '#ccdae8', bg: String(rgf.destination) === o.value ? '#0a5ca8' : '#fff', color: String(rgf.destination) === o.value ? '#fff' : '#3d4c5f', pick: () => this.setRosganLotField('destination', rgf.destination === o.value ? '' : o.value) })),
27597      rgNotFaena: rgFormType !== 'faena', rgShowConfinement: this.rosganShowsConfinement(),
27598      rgDaysOfConfinement: rgf.daysOfConfinement, onRgDaysOfConfinement: rgInput('daysOfConfinement'),
27599      rgBodyConditionOptions, rgHasBodyConditionOptions: !!rgBodyConditionOptions.length, rgHasNoBodyConditionOptions: !rgBodyConditionOptions.length,
27600      rgAnimalClassOptions, rgHasAnimalClassOptions: !!rgAnimalClassOptions.length, rgHasNoAnimalClassOptions: !rgAnimalClassOptions.length,
27601      rgTickStatusOptions, rgHasTickStatusOptions: !!rgTickStatusOptions.length, rgHasNoTickStatusOptions: !rgTickStatusOptions.length,
27602      rgHealthOptions, rgHasHealthOptions: !!rgHealthOptions.length, rgHasNoHealthOptions: !rgHealthOptions.length,
27603      rgNutritionOptions, rgHasNutritionOptions: !!rgNutritionOptions.length, rgHasNoNutritionOptions: !rgNutritionOptions.length,
27604      rgWeighingOptions, rgHasWeighingOptions: !!rgWeighingOptions.length, rgHasNoWeighingOptions: !rgWeighingOptions.length,
27605      rgServiceOptions, rgHasServiceOptions: !!rgServiceOptions.length, rgHasNoServiceOptions: !rgServiceOptions.length,
27606      rgPregnancyOptions, rgHasPregnancyOptions: !!rgPregnancyOptions.length, rgHasNoPregnancyOptions: !rgPregnancyOptions.length,
27607      rgCalvingOptions, rgHasCalvingOptions: !!rgCalvingOptions.length, rgHasNoCalvingOptions: !rgCalvingOptions.length,
27608      rgSubCategoryOptions, rgHasSubCategoryOptions: !!rgSubCategoryOptions.length, rgHasNoSubCategoryOptions: !rgSubCategoryOptions.length,
27609      rgConditionTypeOptions, rgHasConditionTypeOptions: !!rgConditionTypeOptions.length, rgHasNoConditionTypeOptions: !rgConditionTypeOptions.length,
27610      rgDeliveryTypeOptions, rgHasDeliveryTypeOptions: !!rgDeliveryTypeOptions.length, rgHasNoDeliveryTypeOptions: !rgDeliveryTypeOptions.length,
27611      rgFinishingOptions, rgHasFinishingOptions: !!rgFinishingOptions.length, rgHasNoFinishingOptions: !rgFinishingOptions.length,
27612      rgBreedSearch: s.rosganBreedSearch || '',
27613      onRgBreedSearch: (event) => this.setState({ rosganBreedSearch: event.target.value }),
27614      rgBreedSearchHint: !rgBreedQuery,
27615      rgBreedSearchHasResults: !!rgBreedSearchFamilies.length,
27616      rgBreedNoResults: !!rgBreedQuery && !rgBreedMatches.length,
27617      rgBreedSearchResults: rgBreedSearchFamilies.map((group) => {
27618        const view = rgBreedFamilyView(group);
27619        return {
27620          key: group.key, name: group.label, hasColors: !!group.supportsColors, icon: rgBreedIconMap[group.key] || '', hasIcon: !!rgBreedIconMap[group.key],
27621          pick: () => this.selectRosganBreedFamily(group.key),
27622          bg: view.selected ?'#e7f2ff' : '#f4f7fb', color: view.selected ?'#07589d' : '#334a62', border: view.selected ?'#70aee2' : '#d6e1ec',
27623        };
27624      }),
27625      rgHasSelectedBreedFamilies: !!rgSelectedBreedFamilies.length,
27626      rgSelectedBreedFamilies: rgSelectedBreedFamilies.map((group) => {
27627        const view = rgBreedFamilyView(group);
27628        return {
27629          key: group.key, name: group.label, hasColors: !!group.supportsColors, icon: rgBreedIconMap[group.key] || '', hasIcon: !!rgBreedIconMap[group.key],
27630          showBlack: !!group.supportsColors && !!view.blackSelected,
27631          showRed: !!group.supportsColors && !!view.redSelected,
27632          blackAvailable: !!group.black, redAvailable: !!group.red,
27633          blackBg: view.blackSelected ?'#16191d' : '#f1f3f5', blackColor: view.blackSelected ?'#fff' : '#1f2933', blackBorder: view.blackSelected ?'#16191d' : '#c8d0d8',
27634          redBg: view.redSelected ?'#a84735' : '#faefec', redColor: view.redSelected ?'#fff' : '#8d3b2d', redBorder: view.redSelected ?'#a84735' : '#dfb5ab',
27635          toggleBlack: () => this.toggleRosganBreedColor(group.key, 'black'), toggleRed: () => this.toggleRosganBreedColor(group.key, 'red'),
27636          crossesSelected: !!rgBreedCrosses[group.key],
27637          crossesBg: rgBreedCrosses[group.key] ?'#07589d' : '#fff', crossesColor: rgBreedCrosses[group.key] ?'#fff' : '#42617f', crossesBorder: rgBreedCrosses[group.key] ?'#07589d' : '#b9cada',
27638          toggleCrosses: () => this.toggleRosganBreedCrosses(group.key),
27639          remove: () => this.removeRosganBreedFamily(group.key),
27640        };
27641      }),
27642      rgBreedColorVisible: !!rgActiveBreedView?.supportsColors,
27643      rgActiveBreedLabel: rgActiveBreedView?.label || '',
27644      rgBreedBlackAvailable: !!rgActiveBreedView?.black,
27645      rgBreedRedAvailable: !!rgActiveBreedView?.red,
27646      rgBreedBlackBg: rgActiveBreedView?.blackSelected ?'#16191d' : '#f1f3f5',
27647      rgBreedBlackColor: rgActiveBreedView?.blackSelected ?'#fff' : '#1f2933',
27648      rgBreedBlackBorder: rgActiveBreedView?.blackSelected ?'#16191d' : '#c8d0d8',
27649      rgBreedRedBg: rgActiveBreedView?.redSelected ?'#a84735' : '#faefec',
27650      rgBreedRedColor: rgActiveBreedView?.redSelected ?'#fff' : '#8d3b2d',
27651      rgBreedRedBorder: rgActiveBreedView?.redSelected ?'#a84735' : '#dfb5ab',
27652      toggleRgBreedBlack: () => rgActiveBreedView && this.toggleRosganBreedColor(rgActiveBreedView.key, 'black'),
27653      toggleRgBreedRed: () => rgActiveBreedView && this.toggleRosganBreedColor(rgActiveBreedView.key, 'red'),
27654      rgBreedsCount: String(rgSelectedBreedFamilies.length),
27655      rgRemateId: rgf.remateId, rgConsignatariaId: rgf.consignatariaId, rgComitenteId: rgf.comitenteId, rgEstablishmentId: rgf.establishmentId,
27656      rgCertifiedBy: rgf.certifiedBy || '',
27657      rgCertifierDisabled: !rgf.consignatariaId || !!s.rosganCertifiersLoading,
27658      rgCertifierStatus: s.rosganCertifiersError || (!rgf.consignatariaId ?'Seleccioná una consignataria' : s.rosganCertifiersLoading ?'Cargando responsables comerciales…' : !(s.rosganCertifiers || []).length ?'No hay responsables comerciales disponibles' : ''),
27659      rgCertifierOptions,
27660      rgSelectedCertifier: rgCertifierOptions.find(c=>c.selected)||rgCertifierOptions[0],
27661      rgCertifierMenuOpen: !!s.rosganCertifierMenuOpen && !!rgf.consignatariaId && !s.rosganCertifiersLoading,
27662      toggleRgCertifierMenu:()=>{if(rgf.consignatariaId&&!s.rosganCertifiersLoading)this.setState({rosganCertifierMenuOpen:!s.rosganCertifierMenuOpen});},
27663      onRgCertifierKey:event=>{if(event.key==='Escape')this.setState({rosganCertifierMenuOpen:false});},
27664      onRgCertifierChange: event => this.setState({ rosganLotForm: { ...this.rosganForm(), certifiedBy: event.target.value || null } }),
27665      rgConsignatariaDisabled: !rgConsignatariaOptions.length || !!s.rosganEditingLot,
27666      rgConsignatariaMenuOpen: !!s.rosganConsignatariaMenuOpen && !!rgConsignatariaOptions.length,
27667      rgConsignatariaMenuExpanded: s.rosganConsignatariaMenuOpen ?'true' : 'false',
27668      rgConsignatariaChevronRotation: s.rosganConsignatariaMenuOpen ?'180deg' : '0deg',
27669      rgSelectedConsignatariaName: rgSelectedConsignataria?.name || 'Seleccionar…',
27670      rgSelectedConsignatariaLogo: rgSelectedConsignataria?.logo || '',
27671      rgSelectedConsignatariaInitials: rgSelectedConsignataria?.initials || 'C',
27672      rgSelectedConsignatariaHasLogo: !!rgSelectedConsignataria?.logo,
27673      rgSelectedConsignatariaShowsInitials: !rgSelectedConsignataria?.logo,
27674      toggleRgConsignatariaMenu: () => this.setState({ rosganConsignatariaMenuOpen: !this.state.rosganConsignatariaMenuOpen }),
27675      rgCanCreateComitente: this.canManageComitentes(s.apiUser),
27676      rgComitenteFormOpen: !!s.rosganComitenteFormOpen && this.canManageComitentes(s.apiUser),
27677      toggleRgComitenteForm: () => { this.toggleRosganComitenteForm(); if(this.state.rosganComitenteFormOpen) requestAnimationFrame(()=>document.getElementById('rg-inline-comitente')?.scrollIntoView({behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'nearest'})); },
27678      openRgLegacyComitentes: () => setTimeout(() => window.RosganLegacyComitentes?.open({ consignatariaId: this.rosganForm().consignatariaId, apiFetch: this.apiFetch.bind(this), onChanged: () => this.loadRosganLotRelations() }), 0),
27679      openRgComitenteMerge: () => this.openComitenteMerge(true),
27680      rgComitenteCreateDisabled: !rgRelationScope,
27681      rgCanEditComitenteAlias: !!rgComitente && this.canManageComitentes(s.apiUser),
27682      editRgComitenteAlias: () => this.editComitenteAlias(rgComitente.id),
27683      rgComitenteSearchDisabled: !rgRelationsReady,
27684      rgComitenteCreateOpacity: '1',
27685      rgComitenteSearch,
27686      onRgComitenteSearch: (event) => this.onRosganComitenteSearch(event.target.value),
27687      openRgComitenteMenu: () => this.openRosganComitenteMenu(),
27688      closeRgComitenteMenu: event => this.closeRosganComitenteMenu(event),
27689      rgComitenteMenuOpen: !!s.rosganComitenteMenuOpen && rgRelationsReady,
27690      rgFilteredComitentes,
27691      rgComitenteHasMore: rgComitenteMatches.length > rgFilteredComitentes.length,
27692      moreRgComitentes: () => this.setState({ rosganComitenteLimit: (s.rosganComitenteLimit || 60) + 60 }),
27693      keepRgComitenteMenu: e => { if (e.button === 0) e.preventDefault(); },
27694      rgComitenteNoResults: !rgFilteredComitentes.length,
27695      rgComitenteCuit: rgComitenteForm.cuit || '',
27696      rgComitenteAlias: rgComitenteForm.alias || '', onRgComitenteAlias: e => this.setRosganComitenteField('alias', e.target.value),
27697      rgComitenteName: rgComitenteForm.name || '',
27698      rgComitenteFiscalName: rgComitenteForm.fiscalName || '',
27699      rgComitenteProvince: rgComitenteForm.province || '',
27700      rgComitenteLocality: rgComitenteForm.locality || '',
27701      onRgComitenteCuit: (event) => this.onRosganComitenteCuit(event.target.value),
27702      onRgComitenteName: (event) => this.setRosganComitenteField('name', event.target.value, true),
27703      onRgComitenteFiscalName: (event) => this.setRosganComitenteField('fiscalName', event.target.value, true),
27704      onRgComitenteProvince: (event) => this.setRosganComitenteField('province', event.target.value),
27705      onRgComitenteLocality: (event) => this.setRosganComitenteField('locality', event.target.value),
27706      rgComitenteProvinceOptions: s.geoProvinces?.length ?s.geoProvinces : ARGENTINA_PROVINCES,
27707      rgComitenteLocalityOptions,
27708      rgComitenteFiscalLocked,
27709      rgComitenteFiscalBg: rgComitenteFiscalLocked ?'#eef2f6' : '#fff',
27710      rgComitenteLocalityDisabled,
27711      rgComitenteLocalityBg: rgComitenteLocalityDisabled ?'#eef2f6' : '#fff',
27712      rgComitenteLocalityPlaceholder: !rgComitenteForm.province ?'Elegí provincia primero' : (s.geoLoadingLoc ?'Cargando localidades…' : 'Seleccionar…'),
27713      rgComitenteMessageVisible: !!s.rosganComitenteMessage,
27714      rgComitenteMessage: s.rosganComitenteMessage?.text || '',
27715      rgComitenteMessageBg: (s.rosganComitenteMessage?.ok || s.rosganComitenteMessage?.kind === 'success') ?'#e7f5ef' : (s.rosganComitenteMessage?.kind === 'warning' ?'#fff8e5' : (s.rosganComitenteMessage?.kind === 'info' ?'#eaf4ff' : '#fff0f1')),
27716      rgComitenteMessageColor: (s.rosganComitenteMessage?.ok || s.rosganComitenteMessage?.kind === 'success') ?'#176c58' : (s.rosganComitenteMessage?.kind === 'warning' ?'#8a5a00' : (s.rosganComitenteMessage?.kind === 'info' ?'#07589d' : '#b42335')),
27717      rgComitenteSaving: !!s.rosganComitenteSaving,
27718      rgComitenteLookupLoading: !!s.rosganComitenteLookupLoading,
27719      rgComitenteBusy: !!s.rosganComitenteSaving || !!s.rosganComitenteLookupLoading,
27720      rgComitenteSaveDisabled: !!s.rosganComitenteSaving || !rgComitenteReady,
27721      rgComitenteSaveCursor: (s.rosganComitenteSaving || !rgComitenteReady) ?'not-allowed' : 'pointer',
27722      rgComitenteSaveOpacity: (s.rosganComitenteSaving || !rgComitenteReady) ?'.55' : '1',
27723      rgComitenteSaveLabel: s.rosganComitenteSaving ?'Creando…' : (s.rosganComitenteLookupLoading ?'Consultando…' : (!rgComitenteForm.source ?'Esperando validación ARCA' : (!rgComitenteReady ? 'Datos fiscales incompletos' : 'Crear comitente'))),
27724      saveRgComitente: () => this.saveRosganComitente(),
27725      rgCanCreateEstablishment: this.canManageComitentes(s.apiUser),
27726      rgEstablishmentDisabled: !rgRelationsReady || !rgf.comitenteId,
27727      rgEstablishmentPlaceholder: !rgf.consignatariaId ?'Elegí una consignataria primero' : (rgAvailableEstablishments.length ?'Seleccionar establecimiento…' : 'Sin establecimientos para este comitente'),
27728      rgEstablishmentFormOpen: !!s.rosganEstablishmentFormOpen,
27729      toggleRgEstablishmentForm: () =>
27729 { this.toggleRosganEstablishmentForm(); if(this.state.rosganEstablishmentFormOpen) requestAnimationFrame(()=>document.getElementById('rg-inline-establishment')?.scrollIntoView({behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'nearest'})); },
27730      rgEstablishmentName: rgEstablishmentForm.nombre || '',
27731      rgEstablishmentProvince: rgEstablishmentForm.provincia || '',
27732      rgEstablishmentLocality: rgEstablishmentForm.localidad || '',
27733      rgEstablishmentRenspa: rgEstablishmentForm.renspa || '',
27734      onRgEstablishmentName: (event) => this.setRosganEstablishmentField('nombre', event.target.value),
27735      onRgEstablishmentProvince: (event) => this.setRosganEstablishmentField('provincia', event.target.value),
27736      onRgEstablishmentLocality: (event) => this.onRosganEstPlaceInput(event.target.value),
27737      onRgEstablishmentRenspa: (event) => this.setRosganEstablishmentField('renspa', event.target.value),
27738      rgEstablishmentProvinceOptions: s.geoProvinces?.length ?s.geoProvinces : ARGENTINA_PROVINCES,
27739      rgEstablishmentLocalityOptions: [],
27740      rgEstablishmentLocalityDisabled: !rgEstablishmentForm.provincia,
27741      rgEstablishmentLocalityBg: !rgEstablishmentForm.provincia ?'#eef2f6' : '#fff',
27742      rgEstablishmentLocalityPlaceholder: !rgEstablishmentForm.provincia ?'Elegí provincia primero' : 'Escribí una localidad',
27743      rgEstablishmentPlaceLoading: !!s.rosganEstPlaceLoading,
27744      rgEstablishmentPlacesOpen: !!(s.rosganEstPlaceResults || []).length,
27745      rgEstablishmentPlaces: (s.rosganEstPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectRosganEstPlace(item) })),
27746      openRgEstablishmentPlaces: () => { if (rgEstablishmentForm.provincia && String(rgEstablishmentForm.localidad || '').trim().length >= 3) this.searchRosganEstPlaces(rgEstablishmentForm.localidad, rgEstablishmentForm.provincia); },
27747      closeRgEstablishmentPlaces: () => this.closeRosganEstPlaceResults(),
27748      rgEstablishmentPlaceError: s.rosganEstPlaceError || '',
27749      rgEstablishmentMessageVisible: !!s.rosganEstablishmentMessage,
27750      rgEstablishmentMessage: s.rosganEstablishmentMessage?.text || '',
27751      rgEstablishmentMessageBg: s.rosganEstablishmentMessage?.ok ?'#e7f5ef' : '#fff0f1',
27752      rgEstablishmentMessageColor: s.rosganEstablishmentMessage?.ok ?'#176c58' : '#b42335',
27753      rgEstablishmentSaveDisabled: !!s.rosganEstablishmentSaving,
27754      rgEstablishmentSaveLabel: s.rosganEstablishmentSaving ?'Creando…' : 'Crear establecimiento',
27755      saveRgEstablishment: () => this.saveRosganEstablishment(),
27756      rgCategoryId: rgf.categoryId, rgBreedIds: rgf.breedIds, rgRelationsLoading: !!s.rosganLotRelationsLoading,
27757      onRgRemate: (event) => this.onRosganRemateChange(event.target.value),
27758      onRgConsignataria: (event) => this.onRosganConsignatariaChange(event.target.value),
27759      onRgEstablishment: (event) => this.selectRosganEstablishment(event.target.value), onRgCategory: (event) => this.selectRosganLotCategory(event.target.value),
27760      onRgBreeds: (event) => this.setRosganLotField('breedIds', Array.from(event.target.selectedOptions || []).map((option) => option.value)),
27761      rgIsTotalQuantity: !['male_female', 'mothers_calves'].includes(rgQuantityMode), rgIsMaleFemale: rgQuantityMode === 'male_female', rgIsMothersCalves: rgQuantityMode === 'mothers_calves',
27762      rgQuantity: rgf.quantity, rgMaleQuantity: rgf.maleQuantity, rgFemaleQuantity: rgf.femaleQuantity, rgMotherQuantity: rgf.motherQuantity, rgCalfQuantity: rgf.calfQuantity,
27763      onRgQuantity: rgInput('quantity'), onRgMaleQuantity: rgInput('maleQuantity'), onRgFemaleQuantity: rgInput('femaleQuantity'), onRgMotherQuantity: rgInput('motherQuantity'), onRgCalfQuantity: rgInput('calfQuantity'),
27764      rgPaymentDays: ['30', '60', '90', '120', 'other'].map(day => ({ label: day === 'other' ? 'Otro' : day + ' días', checked: (rgf.paymentTermDays || []).includes(day), toggle: () => { const selected = this.rosganForm().paymentTermDays || []; this.setRosganLotField('paymentTermDays', selected.includes(day) ? selected.filter(value => value !== day) : [...selected, day]); } })),
27765      rgOtherPaymentDay: (rgf.paymentTermDays || []).includes('other'), rgOtherPaymentDayValue: rgf.otherPaymentTermDay || '', onRgOtherPaymentDay: rgInput('otherPaymentTermDay'),
27766      rgPaymentReference: rgf.paymentTermReference || '', onRgPaymentReference: rgInput('paymentTermReference'),
27767      rgOtherPaymentReference: rgf.paymentTermReference === 'other', rgOtherPaymentReferenceValue: rgf.otherPaymentTermReference || '', onRgOtherPaymentReference: rgInput('otherPaymentTermReference'),
27768      rgPreviewPaymentTerm: window.RosganPaymentTerm.format(rgf) || 'Plazo pendiente',
27769      rgDescription: rgf.description, onRgDescription: event => this.setRosganDescription(event),
27770      rgAllowsTermino: !!rgCategory?.allowsTermino && ['inv-terneros', 'inv-terneras', 'inv-terneros-as'].includes(rgCategory?.id), rgAllowsAge: !!rgCategory?.allowsAge, rgAllowsDentition: !!rgCategory?.allowsDentition,
27771      rgAllowsReproductiveStatus: !!rgCategory?.allowsReproductiveStatus, rgDestinationLabel: rgFormType === 'faena' ? 'DESTINO *' : 'DESTINO', rgAllowsDestination: rgFormType === 'faena' || !!rgCategory?.allowsDestination, rgAllowsAptoChina: !!rgCategory?.allowsAptoChina,
27772      rgIsTermino: !!rgf.isTermino, rgIsAptoChina: !!rgf.isAptoChina, rgHasMioMio: !!rgf.hasMioMio, rgHasLiquidBrand: !!rgf.hasLiquidBrand, rgIsAptoVisec: !!rgf.isAptoVisec,
27773      rgTerminoPressed: rgf.isTermino ?'true' : 'false', rgTerminoBg: rgf.isTermino ?'#0a5ca8' : '#eef2f5', rgTerminoColor: rgf.isTermino ?'#fff' : '#52606d', rgTerminoBorder: rgf.isTermino ?'#084d8c' : '#d5dde5', rgTerminoShadow: rgf.isTermino ?'0 7px 17px rgba(10,92,168,.22)' : 'none', rgTerminoIconFilter: rgf.isTermino ?'brightness(0) invert(1)' : 'none', rgTerminoIconOpacity: rgf.isTermino ?'1' : '.68', rgTerminoCheckBg: rgf.isTermino ?'rgba(255,255,255,.2)' : '#d8dfe6', rgTerminoCheckColor: rgf.isTermino ?'#fff' : '#8b98a5', rgTerminoCheck: rgf.isTermino ?'✓' : '',
27774      rgChinaPressed: rgf.isAptoChina ?'true' : 'false', rgChinaBg: rgf.isAptoChina ?'#d71920' : '#eef2f5', rgChinaColor: rgf.isAptoChina ?'#fff' : '#52606d', rgChinaBorder: rgf.isAptoChina ?'#b81218' : '#d5dde5', rgChinaShadow: rgf.isAptoChina ?'0 7px 17px rgba(215,25,32,.22)' : 'none', rgChinaFlagFilter: rgf.isAptoChina ?'none' : 'grayscale(1)', rgChinaFlagOpacity: rgf.isAptoChina ?'1' : '.55', rgChinaCheckBg: rgf.isAptoChina ?'rgba(255,255,255,.2)' : '#d8dfe6', rgChinaCheckColor: rgf.isAptoChina ?'#fff' : '#8b98a5', rgChinaCheck: rgf.isAptoChina ?'✓' : '',
27775      rgMioValue: typeof rgf.hasMioMio === 'boolean' ? (rgf.hasMioMio ? 'yes' : 'no') : '',
27776      onRgMio: (event) => this.setRosganLotField('hasMioMio', event.target.value === '' ? null : event.target.value === 'yes'),
27777      rgMioPressed: rgf.hasMioMio ?'true' : 'false', rgMioBg: rgf.hasMioMio ?'#a84412' : '#eef2f5', rgMioColor: rgf.hasMioMio ?'#fff' : '#52606d', rgMioBorder: rgf.hasMioMio ?'#84350e' : '#d5dde5', rgMioShadow: rgf.hasMioMio ?'0 7px 17px rgba(168,68,18,.22)' : 'none', rgMioIconFilter: rgf.hasMioMio ?'brightness(0) invert(1)' : 'none', rgMioIconOpacity: rgf.hasMioMio ?'1' : '.72', rgMioCheckBg: rgf.hasMioMio ?'rgba(255,255,255,.2)' : '#d8dfe6', rgMioCheckColor: rgf.hasMioMio ?'#fff' : '#8b98a5', rgMioCheck: rgf.hasMioMio ?'✓' : '',
27778      rgLiquidPressed: rgf.hasLiquidBrand ?'true' : 'false', rgLiquidBg: rgf.hasLiquidBrand ?'#8a4b24' : '#eef2f5', rgLiquidColor: rgf.hasLiquidBrand ?'#fff' : '#52606d', rgLiquidBorder: rgf.hasLiquidBrand ?'#6f3919' : '#d5dde5', rgLiquidShadow: rgf.hasLiquidBrand ?'0 7px 17px rgba(138,75,36,.22)' : 'none', rgLiquidIconFilter: rgf.hasLiquidBrand ?'brightness(0) invert(1)' : 'none', rgLiquidIconOpacity: rgf.hasLiquidBrand ?'1' : '.68', rgLiquidCheckBg: rgf.hasLiquidBrand ?'rgba(255,255,255,.2)' : '#d8dfe6', rgLiquidCheckColor: rgf.hasLiquidBrand ?'#fff' : '#8b98a5', rgLiquidCheck: rgf.hasLiquidBrand ?'✓' : '',
27779      rgVisecPressed: rgf.isAptoVisec ?'true' : 'false', rgVisecBg: rgf.isAptoVisec ?'#f7faef' : '#eef2f5', rgVisecColor: rgf.isAptoVisec ?'#2b404c' : '#52606d', rgVisecBorder: rgf.isAptoVisec ?'#90b436' : '#d5dde5', rgVisecShadow: rgf.isAptoVisec ?'0 7px 17px rgba(144,180,54,.24)' : 'none', rgVisecIconFilter: rgf.isAptoVisec ?'none' : 'grayscale(1)', rgVisecIconOpacity: rgf.isAptoVisec ?'1' : '.55', rgVisecCheckBg: rgf.isAptoVisec ?'#90b436' : '#d8dfe6', rgVisecCheckColor: rgf.isAptoVisec ?'#fff' : '#8b98a5', rgVisecCheck: rgf.isAptoVisec ?'✓' : '',
27780      toggleRgTermino: rgToggle('isTermino'), toggleRgChina: rgToggle('isAptoChina'), toggleRgMio: rgToggle('hasMioMio'), toggleRgLiquid: rgToggle('hasLiquidBrand'), toggleRgVisec: rgToggle('isAptoVisec'),
27781      rgAgeValue: rgf.ageValue, rgAgeUnit: rgf.ageUnit, rgDentition: rgf.dentition, rgReproductiveStatus: rgf.reproductiveStatus, rgDestination: rgf.destination,
27782      onRgAgeValue: rgInput('ageValue'), onRgAgeUnit: rgInput('ageUnit'), onRgDentition: rgInput('dentition'), onRgReproductiveStatus: rgInput('reproductiveStatus'), onRgDestination: rgInput('destination'),
27783      rgBodyCondition: rgf.bodyCondition, rgAnimalClass: rgf.animalClass, rgTickStatus: rgf.tickStatus, rgHealth: rgf.health, rgNutrition: rgf.nutrition,
27784      onRgBodyCondition: rgInput('bodyCondition'), onRgAnimalClass: rgInput('animalClass'), onRgTickStatus: rgInput('tickStatus'), onRgHealth: rgInput('health'), onRgNutrition: rgInput('nutrition'),
27785      rgIsInvernada: rgFormType === 'invernada', rgIsVientres: rgFormType === 'vientres', rgIsFaena: rgFormType === 'faena', rgHasCategory: !!rgCategory,
27786      rgAverageWeight: rgf.averageWeight, rgMaxWeight: rgf.maxWeight, rgMinWeight: rgf.minWeight, rgWeighing: rgf.weighing, rgDesbaste: rgf.desbaste,
27787      onRgAverageWeight: rgInput('averageWeight'), onRgMaxWeight: rgInput('maxWeight'), onRgMinWeight: rgInput('minWeight'), onRgWeighing: rgInput('weighing'), onRgDesbaste: rgInput('desbaste'),
27788      rgService: rgf.service, rgPregnancy: rgf.pregnancy, rgCalving: rgf.calving, onRgService: rgInput('service'), onRgPregn
27788ancy: rgInput('pregnancy'), onRgCalving: rgInput('calving'),
27789      rgSubCategory: rgf.subCategory, rgConditionType: rgf.conditionType, rgDeliveryType: rgf.deliveryType, rgFinishing: rgf.finishing, rgEstimatedYield: rgf.estimatedYield,
27790      onRgSubCategory: rgInput('subCategory'), onRgConditionType: rgInput('conditionType'), onRgDeliveryType: rgInput('deliveryType'), onRgFinishing: rgInput('finishing'), onRgEstimatedYield: rgInput('estimatedYield'),
27791      rosganLotPublishing: !!s.rosganLotPublishing, rosganLotPublishLabel: s.rosganLotPublishing ?'Cargando lote…' : 'Cargar lote',
27792      rosganLotPublishOpacity: s.rosganLotPublishing || rgCreatedVideoBusy ?'.58' : '1',
27793      publishRosganLot: () => this.publishRosganLot(), resetRosganLot: () => this.resetRosganLotForm(),
27794      showCreateRemate: this.canCreateCabanaRemate(s.apiUser),
27795      showCabanaStatsNavigation: this.canSeeCabanaStatsNavigation(),
27796      createCabanaRemate: () => this.openRemateConfig('Remate Cabañas'),
27797      openAllCbStats: () => this.openCabanaStatsIndex(),
27798      hasRemateCatalog,
27799      remateCatalogName: rc.catalogoPdfName || 'Sin catálogo seleccionado',
27800      remateCatalogMeta: hasRemateCatalog ?`${remateCatalogSizeMb.toFixed(remateCatalogSizeMb >= 10 ?0 : 1)} MB · PDF` : 'Archivo PDF de hasta 30 MB',
27801      remateCatalogAction: hasRemateCatalog ?'Reemplazar PDF' : 'Seleccionar PDF',
27802      onRemateCatalogFile: (e) => this.readRemateCatalog(e.target.files?.[0]),
27803      removeRemateCatalog: () => this.removeRemateCatalog(),
27804      rcCatalogPrivate: !!rcToggles.requerirRegistro,
27805      rcToggleCatalogPrivate: () => this.toggleRemateConfig('requerirRegistro'),
27806      rcCatalogAccessLabel: rcToggles.requerirRegistro ? 'Bajo registro · Visitas identificadas' : 'Público · Usuarios e invitados',
27807      rcCatalogSwitchBg: rcToggles.requerirRegistro ? '#0a5ca8' : '#bccbd9', rcCatalogSwitchOffset: rcToggles.requerirRegistro ? '22px' : '0px',
27808      rcHasSavedRemate: !!s.remateConfigEditingId, rcDetailsMode: !s.rcSchemaEditor || !s.remateConfigEditingId, rcSchemaMode: !!s.rcSchemaEditor && !!s.remateConfigEditingId,
27809      rcDetailsTabBg: s.rcSchemaEditor ?'#fff':'#0a5ca8', rcDetailsTabColor: s.rcSchemaEditor ?'#12304d':'#fff', rcSchemaTabBg: s.rcSchemaEditor ?'#0a5ca8':'#fff', rcSchemaTabColor: s.rcSchemaEditor ?'#fff':'#12304d',
27810      rcShowDetails: () => this.setState({rcSchemaEditor:false}), rcShowSchema: () => { if(s.remateConfigEditingId) this.setState({rcSchemaEditor:true}); }, rcSaveSchema: () => this.saveRemateConfig(this.state.remateConfigStatus || 'borrador'),
27811      rcPlacesExpanded: String(!!s.rcPlaceOpen && !!s.rcPlaceResults?.length), rcPlacesVisible: !!s.rcPlaceOpen && !!s.rcPlaceResults?.length,
27812      rcPlaceActiveId: s.rcPlaceIndex >= 0 ?'rc-place-'+s.rcPlaceIndex:'', rcPlaces: (s.rcPlaceResults || []).map((item,index)=>({...item,id:'rc-place-'+index,source:item.sourceLabel || (item.source === 'google'?'Google Places':'Localidad argentina'),selected:String(index === s.rcPlaceIndex),bg:index === s.rcPlaceIndex?'#edf5fc':'#fff',pickPointer:event=>{event.preventDefault();this.selectCabanaRematePlace(item);},pick:()=>this.selectCabanaRematePlace(item)})),
27813      rcLocality: rc.localidad || '', rcLocalityDisabled: !rc.provincia, rcLocalityPlaceholder: rc.provincia ?'Escribí la localidad':'Elegí una provincia primero',
27814      setRcLocality: event => this.onCabanaRematePlaceInput(event.target.value), rcPlaceKey: event => this.cabanaRematePlaceKey(event), rcPlaceMouseDown: event => event.preventDefault(),
27815      rcPlaceFocus: () => { clearTimeout(this._rcPlaceBlur); this.setState({rcPlaceOpen:true}); }, rcPlaceBlur: () => { this._rcPlaceBlur=setTimeout(()=>this.setState({rcPlaceOpen:false}),150); },
27816      rcPlaceStatus: s.rcPlaceLoading ?'Buscando localidades…':s.rcPlaceError || 'Buscá por nombre (mínimo 3 letras).',
27817      rcApproversDisabled: !!rcToggles.apruebaAuto,
27818      rcApproversError: s.adminUsersError || (s.adminUsersLoading ? 'Cargando aprobadores…' : (!rc.consignataria ? 'Seleccioná una consignataria.' : (!this.cabanaEligibleApprovers(rc).length ? 'No hay usuarios habilitados asociados a esta consignataria.' : ''))),
27819      rcApprovers: this.cabanaEligibleApprovers(rc).map(u =>
27819 ({uid:u.uid,label:[u.displayName,u.email].filter(Boolean).join(' · '),checked:(rc.approverUserIds || []).includes(u.uid),toggle:() => {const ids=new Set(rc.approverUserIds || []);if(ids.has(u.uid))ids.delete(u.uid);else ids.add(u.uid);this.setRemateConfigField('approverUserIds',[...ids]);}})),
27820      rcPricing: (rc.formulariosCategoria || []).filter(c => c.categoria).map(c => {const price=(rc.categoryPricing || []).find(p => p.categoryId === c.categoryId) || {}; const set=(key,event) => this.setCabanaCategoryPrice(c.categoryId,key,event.target.value);return {label:c.categoria,disabled:!c.categoryId,base:price.baseAmount ?? '',increment:price.incrementAmount ?? '',onBase:e=>set('baseAmount',e),onIncrement:e=>set('incrementAmount',e)};}),
27821      remateConfigSections, remateConfigOptions, preofferFields, cabanaParticipantGroups, remateCategoryForms, addRemateCategoryForm: () => this.addRemateCategoryForm(),
27822      preofferEnabled: !!rcToggles.preHabilitada,
27823      preofferLabel: rcToggles.preHabilitada ?'Preoferta habilitada' : 'Habilitar preoferta',
27824      preofferBg: rcToggles.preHabilitada ?'#e2f2ee' : '#fff', preofferColor: rcToggles.preHabilitada ?'#1f7a63' : '#65758a', preofferBorder: rcToggles.preHabilitada ?'#a9d7ca' : '#cfe0f5', preofferDot: rcToggles.preHabilitada ?'#1f7a63' : '#9fb0c3',
27825      togglePreoffer: () => this.toggleRemateConfig('preHabilitada'),
27826      saveRemateDraft: () => this.saveRemateConfig('borrador'), publishRemateConfig: () => this.saveRemateConfig('abierto'), resetRemateConfig: () => this.resetRemateConfig(), closeRemateConfig: () => { this.resetCabanaRematePlaces(); this.go('cabanas'); },
27827      remateConfigTitle: s.rcSchemaEditor && s.remateConfigEditingId ?'Formulario de lotes' : s.remateConfigEditingId ?'Editar remate de cabaña' : 'Crear remate de cabaña',
27828      remateConfigSubtitle: s.rcSchemaEditor && s.remateConfigEditingId ?'Configurá las categorías y los campos que se pedirán al cargar lotes.' : s.remateConfigEditingId ?'Actualizá los datos, participantes y configuración del remate.' : 'Definí el remate, sus cabañas participantes, la carga y la preoferta.',
27829      remateConfigBusy: !!s.remateConfigSaving,
27830      remateConfigDraftLabel: s.remateConfigSaving === 'draft' ?'Guardando…' : (s.remateConfigSaved ?'Borrador guardado' : 'Guardar borrador'),
27831      remateConfigPublishLabel: s.remateConfigSaving === 'publish' ?'Publicando…' : (s.remateConfigEditingId ?'Publicar cambios' : 'Publicar remate'),
27832      remateConfigActionOpacity: s.remateConfigSaving ?0.58 : 1,
27833      remateConfigSaveBg: s.remateConfigSaved ?'#1f7a63' : '#e63946',
27834      showDesktopLoad: !s.publicAdvanceMode && !s.compactNavigation && (canLoadLot || this.canManageRosganLots(s.apiUser)) && !['remateConfig', 'cabanaLotCreate', 'rosganLotLoad', 'rosganRemateCreate'].includes(s.route),
27835      showLoadFab: !s.publicAdvanceMode && s.isMobile && (canLoadLot || this.canManageRosganLots(s.apiUser)) && !['load', 'rosganLotLoad', 'rosganRemateCreate', 'login', 'profile', 'mailingAdmin', 'remateConfig', 'cabanaLotCreate', 'remates', 'cabanaLotDetail'].includes(s.route),
27836      isRegister: authMode === 'register', isSignIn: authMode === 'signin', showPassword: authMode !== 'reset',
27837      showAccountAssistantLauncher: !s.accountAssistantOpen && authMode !== 'reset',
27838      showAuthNameField: authMode === 'register' && (!accountAssistantEmailActive || accountAssistantStep >= 1),
27839      showAuthPhoneField: authMode === 'register' && (!accountAssistantEmailActive || accountAssistantStep >= 2),
27840      showAuthEmailField: !(s.accountAssistantOpen && authMode === 'signin') && (!accountAssistantEmailActive || accountAssistantStep >= 3),
27841      showAuthPasswordField: !(s.accountAssistantOpen && authMode === 'signin') && (!accountAssistantEmailActive || accountAssistantStep >= 4),
27842      showPrimaryAuthSubmit: !(s.accountAssistantOpen && accountAssistantMethod === 'email'),
27843      showDefaultGoogleAuth: authMode === 'signin' && !s.accountAssistantOpen,
27844      showAuthFooterLinks: !s.accountAssistantOpen,
27845      accountAssistantOpen: !!s.accountAssistantOpen,
27846      accountAssistantTitle: accountAssistantContent?.title || 'Asistente de cuenta',
27847      accountAssistantText: accountAssistantContent?.text || '',
27848      accountAssistantTip: accountAssistantContent?.tip || '',
27849      accountAssistantHasTip: !!accountAssistantContent?.tip,
27850      accountAssistantMessage: s.accountAssistantMessage || '',
27851      accountAssistantIsChoice: !!s.accountAssistantOpen && !accountAssistantMethod,
27852      accountAssistantIsGoogle: !!s.accountAssistantOpen && accountAssistantMethod === 'google',
27853      accountAssistantIsEmail: !!s.accountAssistantOpen && accountAssistantMethod === 'email',
27854      accountAssistantHasProgress: !!s.accountAssistantOpen && accountAssistantMethod === 'email',
27855      accountAssistantProgressLabel: accountAssistantMethod === 'email' ?`Paso ${accountAssistantStep} de 5` : '',
27856      accountAssistantProgress: accountAssistantMethod === 'email' ?`${accountAssistantStep * 20}%` : '0%',
27857      accountAssistantBackLabel: accountAssistantStep <= 1 ?'Elegir otra opción' : 'Atrás',
27858      accountAssistantNextLabel: accountAssistantStep >= 5 ?(s.authBusy ?'Creando…' : 'Crear mi cuenta') : 'Siguiente',
27859      accountAssistantNextDisabled: !!s.authBusy,
27860      openAccountAssistant: () => this.openAccountAssistant(),
27861      closeAccountAssistant: () => this.closeAccountAssistant(),
27862      chooseAccountAssistantGoogle: () => this.chooseAccountAssistantGoogle(),
27863      chooseAccountAssistantEmail: () => this.chooseAccountAssistantEmail(),
27864      backAccountAssistant: () => this.backAccountAssistant(),
27865      nextAccountAssistant: () => this.nextAccountAssistant(),
27866      authNameClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 1 ?' rosgan-auth-field-assisted' : ''),
27867      authPhoneClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 2 ?' rosgan-auth-field-assisted' : ''),
27868      authEmailClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 3 ?' rosgan-auth-field-assisted' : ''),
27869      authPasswordClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 4 ?' rosgan-auth-field-assisted' : ''),
27870      authSubmitClass: s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 5 ?'rosgan-auth-field-assisted' : '',
27871      loginTitle: authMode === 'register' ?'Crear cuenta' : authMode === 'reset' ?'Recuperar contraseña' : 'Bienvenido a Rosgan',
27872      loginSubtitle: authMode === 'register' ?'Completá tus datos y tu teléfono de contacto.' : authMode === 'reset' ?'Te enviaremos un enlace de recupero.' : 'Ingresá para continuar a tu cuenta.',
27873      submitLabel: s.authBusy ?'Procesando…' : (authMode === 'register' ?'Crear cuenta' : authMode === 'reset' ?'Enviar enlace' : 'Ingresar'),
27874      authBusy: !!s.authBusy, authBtnOpacity: s.authBusy ?0.6 : 1,
27875      resetLink: authMode === 'reset' ?'Volver a ingresar' : 'Olvidé mi contraseña',
27876      registerLink: authMode === 'register' ?'¿Ya tengo cuenta?' : 'Crear cuenta',
27877      toggleReset: () => this.setState({ authMode: authMode === 'reset' ?'signin' : 'reset', authMessage: '', authPasswordVisible: false }),
27878      toggleRegister: () => this.setState({ authMode: authMode === 'register' ?'signin' : 'register', authMessage: '', authPasswordVisible: false }),
27879      formName: s.formName, formEmail: s.formEmail, formPassword: s.formPassword, formPhone: s.formPhone,
27880      authPasswordVisible: !!s.authPasswordVisible,
27881      passwordInputType: s.authPasswordVisible ?'text' : 'password',
27882      passwordVisibilityLabel: s.authPasswordVisible ?'Ocultar contraseña' : 'Mostrar c
27882ontraseña',
27883      toggleAuthPasswordVisibility: () => this.setState({ authPasswordVisible: !s.authPasswordVisible }),
27884      passwordAutocomplete: authMode === 'register' ?'new-password' : 'current-password',
27885      passwordLengthValid: s.formPassword.length >= 8,
27886      passwordLengthColor: s.formPassword.length >= 8 ?'#baffdf' : '#d6e4f2',
27887      passwordLengthBorder: s.formPassword.length >= 8 ?'#72e7b3' : 'rgba(255,255,255,.32)',
27888      passwordLengthBg: s.formPassword.length >= 8 ?'rgba(22,184,133,.34)' : 'rgba(255,255,255,.07)',
27889      passwordLengthIconColor: s.formPassword.length >= 8 ?'#ffffff' : '#b8cadb',
27890      passwordUpperValid: /[A-ZÁÉÍÓÚÜÑ]/.test(s.formPassword),
27891      passwordUpperColor: /[A-ZÁÉÍÓÚÜÑ]/.test(s.formPassword) ?'#baffdf' : '#d6e4f2',
27892      passwordUpperBorder: /[A-ZÁÉÍÓÚÜÑ]/.test(s.formPassword) ?'#72e7b3' : 'rgba(255,255,255,.32)',
27893      passwordUpperBg: /[A-ZÁÉÍÓÚÜÑ]/.test(s.formPassword) ?'rgba(22,184,133,.34)' : 'rgba(255,255,255,.07)',
27894      passwordUpperIconColor: /[A-ZÁÉÍÓÚÜÑ]/.test(s.formPassword) ?'#ffffff' : '#b8cadb',
27895      onName: (e) => this.bufferInputState('auth-name', { formName: e.target.value }),
27896      onEmail: (e) => this.bufferInputState('auth-email', { formEmail: e.target.value }),
27897      onPassword: (e) => this.bufferInputState('auth-password', { formPassword: e.target.value }),
27898      onFormPhone: (e) => this.bufferInputState('auth-phone', { formPhone: e.target.value }),
27899      submitAuthForm: (event) => {
27900        event.preventDefault();
27901        if (!s.authBusy) this.submitAuth();
27902      },
27903      submitAuthOnEnter: (event) => {
27904        if (event.key !== 'Enter' || event.isComposing || event.nativeEvent?.isComposing) return;
27905        event.preventDefault();
27906        if (!s.authBusy) this.submitAuth();
27907      },
27908      submitAuth: () => this.submitAuth(), googleAuth: () => this.googleAuth(),
27909      authMessage: s.authMessage,
27910      emailVerificationEmail: s.user?.email || s.formEmail,
27911      emailVerificationBusy: !!s.emailVerificationBusy,
27912      emailVerificationMessage: s.emailVerificationMessage,
27913      emailVerificationOpacity: s.emailVerificationBusy ?0.58 : 1,
27914      emailVerificationCheckLabel: s.emailVerificationBusy ?'Comprobando…' : 'Ya verifiqué mi correo',
27915      emailVerificationResendLabel: s.emailVerificationBusy ?'Procesando…' : 'Reenviar correo',
27916      checkEmailVerification: () => this.checkEmailVerification(),
27917      resendEmailVerification: () => this.resendEmailVerification(),
27918      logoutFromEmailVerification: () => this.logoutFromEmailVerification(),
27919      isPhoneEntryStep: s.phoneVerificationStep !== 'code',
27920      isPhoneCodeStep: s.phoneVerificationStep === 'code',
27921      phoneVerificationNumber: s.phoneVerificationNumber,
27922      phoneVerificationCode: s.phoneVerificationCode,
27923      phoneVerificationBusy: !!s.phoneVerificationBusy,
27924      phoneVerificationMessage: s.phoneVerificationMessage,
27925      phoneVerificationLocalhost: ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname),
27926      phoneVerificationMasked: this.maskedPhoneNumber(s.phoneVerificationNumber),
27927      phoneVerificationOpacity: s.phoneVerificationBusy ?0.58 : 1,
27928      phoneVerificationSendLabel: s.phoneVerificationBusy ?'Guardando…' : 'Guardar y continuar',
27929      phoneVerificationVerifyLabel: s.phoneVerificationBusy ?'Verificando…' : 'Verificar y continuar',
27930      onPhoneVerificationNumber: (e) => this.setState({ phoneVerificationNumber: e.target.value, phoneVerificationMessage: '' }),
27931      onPhoneVerificationCode: (e) => this.setState({ phoneVerificationCode: String(e.target.value || '').replace(/\D/g, '').slice(0, 6), phoneVerificationMessage: '' }),
27932      savePhoneAndContinue: () => this.savePhoneAndContinue(),
27933      changePhoneNumber: () => this.changePhoneNumber(),
27934      logoutFromPhoneVerification: () => this.logoutFromPhoneVerification(),
27935      logout: () => {
27936        this._sessionEpoch = (this._sessionEpoch || 0) + 1;
27937        this._pathRequest = (this._pathRequest || 0) + 1;
27938        this._initialPathname = '/perfil';
27939        this._initialPathRoute = 'profile';
27940        this._initialSearch = '';
27941        this.writeAppHistory('/perfil', { replace: true });
27942        try { sessionStorage.removeItem('rosgan:lastRoute'); } catch {}
27943        this.clearWelcomeAfterSignIn();
27944        this.clearCachedApiUser();
27945        if (this._welcomeTimer) { clearTimeout(this._welcomeTimer); this._welcomeTimer = null; }
27946        this._phoneConfirmationResult = null;
27947        this._phoneVerificationId = null;
27948        this.clearPhoneRecaptcha();
27949        this.signOutSession();
27950        this.setState({ user: null, route: 'login', authReady: true, authMode: 'signin', emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', phoneVerificationRequired: false, phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationBusy: false, phoneVerificationMessage: '', notificationsItems: null, notificationsOpen: false, lau
27950nchInterest: null, launchInterestLoading: false, launchInterestSaving: false, launchInterestMessage: '' });
27951      },
27952      userInitials: (currentProfileName || s.user?.displayName || s.user?.email || '?').split(/[\s@.]+/).slice(0, 2).map((w) => w[0]?.toUpperCase() || '').join(''),
27953      userHasConsignatariaAvatar,
27954      userShowsInitials: !userHasConsignatariaAvatar,
27955      userConsignatariaAvatar,
27956      userConsignatariaName: userConsignatariaMatch?.name || userConsignatariaMatch?.nombre || s.userConsignataria?.name || userConsignatariaRef,
27957      userAvatarBg: userHasConsignatariaAvatar ?currentAvatar.bg : '#0a5ca8',
27958      userAvatarBorder: userHasConsignatariaAvatar ?'rgba(255,255,255,0.72)' : '#8fb6dc',
27959      userAvatarNavSize: userHasProfilePhoto ?'100%' : '27px',
27960      userAvatarHeroSize: userHasProfilePhoto ?'100%' : '52px',
27961      userAvatarObjectFit: userHasProfilePhoto ?'cover' : 'contain',
27962      profilePhotoViewerOpen: !s.publicAdvanceMode && !!s.profilePhotoViewerOpen && userHasConsignatariaAvatar,
27963      profilePhotoViewerUrl: userConsignatariaAvatar,
27964      profilePhotoViewerAlt: currentProfileName || userConsignatariaMatch?.name || userConsignatariaMatch?.nombre || 'Foto de perfil',
27965      profileAvatarCursor: userHasConsignatariaAvatar ?'zoom-in' : 'default',
27966      openProfilePhotoViewer: (event) => this.openProfilePhotoViewer(event),
27967      closeProfilePhotoViewer: () => this.closeProfilePhotoViewer(),
27968      parallaxY: (anim ?Math.round(s.parallax * 0.35) : 0) + 'px',
27969      logoH: s.isMobile ?'22px' : '30px',
27970      homePadBottom: s.compactNavigation ?'calc(90px + env(safe-area-inset-bottom))' : '20px',
27971      heroPadBottom: s.isMobile ?'64px' : '72px',
27972      accessPull: s.isMobile ?'12px' : '-58px',
27973      homeHeroLoading: s.monthRemates === null,
27974      homeHeroReady: s.monthRemates !== null,
27975      homeIndicesLoading: !Array.isArray(s.indices),
27976      homeIndicesReady: Array.isArray(s.indices) && homeIndexCards.length > 0,
27977      homeIndicesEmpty: Array.isArray(s.indices) && homeIndexCards.length === 0,
27978      homeMonthLoading: s.monthRemates === null,
27979      homeAgendaLoading: s.ferias === null,
27980      homeAgendaReady: s.ferias !== null,
27981      homeVideosLoading: s.videos === null,
27982      homeCafeVideosLoading: s.cafeVideos === null,
27983      homeLoteNewsLoading: false, // The local interactive edition is available before the PDF archive arrives.
27984      homeNewsLoading: s.noticias == null,
27985      ...(s.route === 'home' ? this.homeChannelView() : {}),
27986      homeChannelsLoading: s.consignatarias === null,
27987      homeChannelsReady: s.consignatarias !== null,
27988      homeIndexSkeletons: Array.from({ length: 3 }, (_, i) => ({ key: 'hi' + i })),
27989      homeAgendaSkeletons: Array.from({ length: 3 }, (_, i) => ({ key: 'ha' + i })),
27990      homeSectionSkeletons: Array.from({ length: 4 }, (_, i) => ({ key: 'hs' + i })),
27991      homeChannelSkeletons: Array.from({ length: 6 }, (_, i) => ({ key: 'hc' + i })),
27992      ...(() => {
27993        const monthAll = this.homeFeaturedRemates(s.monthRemates, s.rosganRemates).filter(it => !it.__feriaAuction && this.remateType(it)[0] !== 'Feria').map((it) => ({ it, mode: this.histMode(it), heroImage: this.histThumb(it) }));
27994        const cabanaSlides = (s.cabanas || [])
27995          .filter(it => !this.isExcludedCabana(it) && it.activo !== false && it.closeRemate !== true && this.cabanaMode(it) !== 'recent')
27996          .map(cabana => ({ cabana, it: { ...cabana, nombre_remate: cabana.nombre_remate || cabana.title || cabana.titulo }, mode: this.cabanaMode(cabana), heroImage: this.cabanaMedia(cabana) || 'assets/angus-field.png' }));
27997        const cabanaTitles = new Set(cabanaSlides.map(({ it }) => this.consKey(it.nombre_remate)));
27998        const slideDate = slide => slide.cabana ?this.cabanaAuctionStartAt(slide.cabana) : this.remateDate(slide.it);
27999        const slidesRaw = [{ congress: true }, ...[...monthAll.filter(({ it }) => !cabanaTitles.has(this.consKey(it.nombre_remate))), ...cabanaSlides].sort((a, b) => (slideDate(a) || Infinity) - (slideDate(b) || Infinity))];
28000        this._homeHeroRosganIndices = slidesRaw.flatMap(({it, congress, cabana}, i) => {
28001          if (congress || cabana) return [];
28002          const ref = this.remateConsignatariaRef(it) || 'Rosgan';
28003          const name = this.consRecord(ref)?.name || this.consRecord(ref)?.nombre || it.nombre_cuenta || it.consignatariaNombre || '';
28004          return this.consKey(ref) === 'rosgan' || this.consKey(name) === 'rosgan' ?[i]:[];
28005        });
28006        this._heroCount = slidesRaw.length || 1;
28007        const idx = slidesRaw.length ?(s.heroSlide % slidesRaw.length + slidesRaw.length) % slidesRaw.length : 0;
28008        const active = slidesRaw[idx];
28009        if (active?.congress) return {
28010          heroPrebidsUpcoming:false, heroPrebidsActive:false, heroIsCongress:true, heroIsAuction:false, heroCongressClass:'home-congress', heroSurface:'#6c86fe',
28011          heroHasSlides:slidesRaw.length>1, heroTextKey:'congress-2026',
28012          heroDots:slidesRaw.map((slide,i)=>({go:()=>this.selectHomeHero(i),label:slide.congress?'Congreso Federal Ganadero':slide.it.nombre_remate || 'Remate',active:i===idx,w:i===id
28012x?'28px':'10px',bg:i===idx?'#060f52':'#ffffff99'})),
28013        };
28014
28015        if (active) {
28016          const it = active.it; const live = active.mode === 'live';
28017          const cabana = active.cabana;
28018          const consRef = (cabana ?cabana.consignatariaId || cabana.consignataria || cabana.nombre_cuenta : this.remateConsignatariaRef(it)) || 'Rosgan';
28019          const brandColor = this.consBrandColor(consRef);
28020          const consignatariaHeroLogo = this.consFullHeroLogo(consRef);
28021          const heroConsName = this.consRecord(consRef)?.name || this.consRecord(consRef)?.nombre || it.nombre_cuenta || it.consignatariaNombre || '';
28022          const isRosganHero = !cabana && (this.consKey(consRef) === 'rosgan' || this.consKey(heroConsName) === 'rosgan');
28023          const heroConsLogo = isRosganHero ? 'assets/rosgan-logo-blue.png' : consignatariaHeroLogo;
28024          const anniversaryImage = cabana ?'' : this.rosganAnniversaryImage(it);
28025          const prebidWindow = window.CabanaPrebid.windowState(cabana || it);
28026          return {
28027            heroPrebidsActive:prebidWindow.active, heroPrebidsUpcoming:prebidWindow.upcoming, heroPrebidStart:prebidWindow.start || 0, heroPrebidStartLabel:prebidWindow.upcoming ?new Date(prebidWindow.start).toLocaleString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',weekday:'long',day:'numeric',month:'long',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}) + ' h (Argentina)' : '', heroIsCongress:false, heroIsAuction:true, heroCongressClass:'', heroSurface:'#002e5f',
28028            heroHasSlides: slidesRaw.length > 1, heroTextKey: 'h' + idx,
28029            heroImage: anniversaryImage || (isRosganHero ?this.rosganBanner(Math.max(0,s.homeRosganBannerIndex || 0)).src : (cabana ?this.consBackground(consRef) || this.consBackground(heroConsName) : '') || active.heroImage),
28030            heroImagePosition: anniversaryImage ?'center 48%' : isRosganHero ?this.rosganBanner(Math.max(0,s.homeRosganBannerIndex || 0)).position : this.consBackgroundPosition(consRef, 'banner'),
28031            heroImageMobileHeight: (this.consKey(consRef).includes('ildarraz') || this.consKey(heroConsName).includes('ildarraz')) ?'72%' : '100%',
28032            heroImageMobilePosition: anniversaryImage ?'center 48%' : isRosganHero ?this.rosganBanner(Math.max(0,s.homeRosganBannerIndex || 0)).position : (this.consKey(consRef).includes('ildarraz') || this.consKey(heroConsName).includes('ildarraz')) ?'85% center' : (this.consRecord(consRef)?.backgroundMobilePosition || this.consRecord(consRef)?.bannerMobilePosition || this.consBackgroundPosition(consRef, 'card')),
28033            heroOverlay: 'linear-gradient(90deg, rgba(0,0,0,.76) 0%, rgba(0,0,0,.58) 42%, rgba(0,0,0,.18) 100%)',
28034            heroConsLogo, heroHasConsLogo: !!heroConsLogo, heroConsName: isRosganHero ? 'Rosgan' : heroConsName, heroConsLogoFilter: isRosganHero ? 'brightness(0) invert(1)' : this.consFullHeroLogoFilter(consRef),
28035            heroBadge: live ?'EN VIVO' : cabana ?'REMATE DE CABAÑA' : 'PRÓXIMO REMATE', heroBadgeBg: live ?'rgba(230,57,70,0.94)' : 'rgba(255,255,255,0.17)',
28036            heroTitle: it.nombre_remate || 'Rosgan Streaming',
28037            heroSubtitle: cabana ?[this.cabanaAuctionStartAt(cabana)?.toLocaleString('es-AR', { timeZone:'America/Argentina/Buenos_Aires', weekday:'long', day:'numeric', month:'long', hour:'2-digit', minute:'2-digit', hourCycle:'h23' }), this.cabanaWeatherLocation(cabana)].filter(Boolean).join(' · ') : (this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { weekday: 'long', day: 'numeric', month: 'long' }) + (it.hora_remate ?' · ' + String(it.hora_remate).slice(0, 5) + ' h' : '') : 'Transmisión oficial') + ' · ' + (it.ubicacion_remate || it.nombre_cuenta || 'Rosgan'),
28038            heroHref: cabana ?(cabana.youtube?.watchUrl || (cabana.youtube?.videoId ?'https://www.youtube.com/watch?v=' + cabana.youtube.videoId : this.cabanaPath(cabana))) : this.histUrl(it),
28039            heroOpenLots: () => cabana ?this.openCabana(cabana) : it.__rosganActive ?this.openRosganActiveRemate(it) : this.openRemate(it),
28040            heroStats: [{ value: '$' + fmt(3412 * p), label: 'PROMEDIO/KG' }, { value: fmt(18240 * p), label: 'CABEZAS' }],
28041            heroDots: slidesRaw.map((_, i) => ({ go: () =>
28041 this.selectHomeHero(i), w: i === idx ?'26px' : '6px', bg: i === idx ?'#fff' : 'rgba(255,255,255,0.4)' })),
28042          };
28043        }
28044        return {
28045          heroPrebidsUpcoming:false, heroPrebidsActive:false, heroHasSlides: false, heroTextKey: 'h0', heroImage: 'assets/angus-field.png',
28046          heroImagePosition: 'center',
28047          heroOverlay: 'linear-gradient(90deg, rgba(0,24,52,.96) 0%, rgba(0,35,72,.84) 48%, rgba(0,24,52,.24) 100%)',
28048          heroConsLogo: '', heroHasConsLogo: false, heroConsName: '', heroConsLogoFilter: 'none',
28049          heroBadge: 'AGENDA ROSGAN', heroBadgeBg: 'rgba(10,92,168,0.9)',
28050          heroTitle: 'El próximo encuentro, en Rosgan',
28051          heroSubtitle: 'Consultá los remates y las transmisiones de nuestra red.',
28052          heroHref: 'https://www.youtube.com/@rosgantv',
28053          heroOpenLots: () => this.go('remates'),
28054          heroStats: [{ value: '$' + fmt(3412 * p), label: 'PROMEDIO/KG' }, { value: fmt(18240 * p), label: 'CABEZAS' }],
28055          heroDots: [],
28056        };
28057      })(),
28058      goHome: () => this.go('home'), goPool: () => this.go('pool'), goRemates: () => this.go('remates'),
28059      heroPrevious: () => this.moveHomeHero(-1), heroNext: () => this.moveHomeHero(1),
28060      heroPointerDown: (event) => this.startHomeHeroGesture(event),
28061      heroPointerUp: (event) => this.endHomeHeroGesture(event),
28062      heroPointerCancel: (event) => this.endHomeHeroGesture(event, true),
28063      heroKeyDown: (event) => { if (event.target !== event.currentTarget) return; if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { event.preventDefault(); this.moveHomeHero(event.key === 'ArrowLeft' ?-1 : 1); } },
28064      ...(() => {
28065        const c = s.selectedCons;
28066        const name = c ?this.consName(c) : 'Canal';
28067        const logo = c ?(this.consLogo(name) || c.logoUrl || c.logo || c.imageUrl) : '';
28068        const channelBackground = c ?(c.backgroundUrl || c.bannerUrl || this.consBackground(name)) : '';
28069        const col = this.consColor(name);
28070        const list = (s.consRemates || []).map((it) => ({ it, mode: this.histMode(it) }))
28071          .sort((a, b) => { const o = { live: 0, upcoming: 1, recent: 2 }; const md = o[a.mode] - o[b.mode]; if (md) return md; return a.mode === 'recent' ?(this.remateDate(b.it) || 0) - (this.remateDate(a.it) || 0) : (this.remateDate(a.it) || 0) - (this.remateDate(b.it) || 0); });
28072        const nLive = list.filter((x) => x.mode === 'live').length;
28073        const nUp = list.filter((x) => x.mode === 'upcoming').length;
28074        const nRec = list.filter((x) => x.mode === 'recent').length;
28075        const tab = s.channelTab;
28076        const feat = list.find((x) => x.mode === 'live') || list.find((x) => x.mode === 'upcoming') || list[0];
28077        const shown = tab === 'todos' ?list : list.filter((x) => (tab === 'vivo' ?x.mode === 'live' : tab === 'proximo' ?x.mode === 'upcoming' : x.mode === 'recent'));
28078        const grid = shown.filter((x) => x !== feat);
28079        const badgeFor = (m) => m === 'live' ?{ badge: 'EN VIVO', badgeBg: '#e63946', live: true } : m === 'upcoming' ?{ badge: 'PRÓXIMO', badgeBg: col, live: false } : { badge: 'FINALIZADO', badgeBg: 'rgba(0,29,61,0.72)', live: false };
28080        const fmtD = (it) => this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { weekday: 'short', day: 'numeric', month: 'short' }) + (it.hora_remate ?' · ' + String(it.hora_remate).slice(0, 5) + ' h' : '') : 'Fecha a confirmar';
28081        const tabs = [
28082          { key: 'todos', label: 'Todos', n: list.length }, { key: 'vivo', label: 'En vivo', n: nLive, live: true },
28083          { key: 'proximo', label: 'Próximos', n: nUp }, { key: 'grabado', label: 'Grabados', n: nRec },
28084        ];
28085        // ---- asociación de videos de YouTube al canal ----
28086        // cada remate de la consignataria trae su id de YouTube; con eso filtramos el feed del canal
28087        const ckey = this.consKey(name);
28088        const remYt = (it) => it.youtubeApiId || it.youtubeVideoId || (it.youtube && it.youtube.id) || this.ytId(it.url_streaming || '');
28089        const consYtIds = new Set(list.map((x) =>
28089 remYt(x.it)).filter(Boolean).map(String));
28090        const vidId = (v) => String(v.id || v.videoId || this.ytId(v.url || '') || '');
28091        const vidMatch = (v) => { if (consYtIds.has(vidId(v))) return true; const tk = this.consKey(v.title); return !!(tk && ckey && tk.includes(ckey)); };
28092        const chVids = (s.videos || []).filter(vidMatch).filter((v, i, a) => a.findIndex((w) => vidId(w) === vidId(v)) === i);
28093        const channelOriginalThumb = (it) => {
28094          const id = String(remYt(it) || '');
28095          const matched = id ?chVids.find((v) => vidId(v) === id) : null;
28096          return matched?.thumbnailUrl || matched?.thumbnail || this.youtubeOriginalThumb(it);
28097        };
28098        // ---- videoteca por año (remates grabados con video) ----
28099        const recents = list.filter((x) => x.mode === 'recent');
28100        const yearOf = (x) => { const d = this.remateDate(x.it); return d ?d.getFullYear() : null; };
28101        const libYears = [...new Set(recents.map(yearOf).filter(Boolean))].sort((a, b) => b - a);
28102        const curYear = (s.channelYear && libYears.includes(s.channelYear)) ?s.channelYear : (libYears[0] || null);
28103        const libItems = recents.filter((x) => yearOf(x) === curYear).sort((a, b) => (this.remateDate(b.it) || 0) - (this.remateDate(a.it) || 0));
28104        return {
28105          chName: name, chInitial: name.trim()[0].toUpperCase(),
28106          chLogo: logo, chHasLogo: !!logo, chNoLogo: !logo,
28107          chColor: col, chGradA: this.shade(col, 0.28), chGradB: this.shade(col, -0.45), chGlow: this.shade(col, 0.5),
28108          chHeroOverlay: 'linear-gradient(125deg, ' + this.rgba(this.shade(col, -0.45), 0.93) + ' 0%, ' + this.rgba(col, 0.8) + ' 52%, ' + this.rgba(this.shade(col, 0.28), 0.6) + ' 100%)',
28109          chHeroImages: (() => { const imgs = channelBackground ?[channelBackground] : this.channelHeroImages; const hi = imgs.length ?((s.chHeroSlide % imgs.length) + imgs.length) % imgs.length : 0; return imgs.map((src, i) => ({ src, opacity: i === hi ?1 : 0, position: channelBackground ?this.consBackgroundPosition(name, 'banner') : 'center' })); })(),
28110          chHasHeroDots: !channelBackground && this.channelHeroImages.length > 1,
28111          chHeroDots: (() => { const imgs = channelBackground ?[channelBackground] : this.channelHeroImages; const hi = imgs.length ?((s.chHeroSlide % imgs.length) + imgs.length) % imgs.length : 0; return imgs.map((_, i) => ({ go: () => this.setState({ chHeroSlide: i }), w: i === hi ?'22px' : '6px', bg: i === hi ?'#fff' : 'rgba(255,255,255,0.45)' })); })(),
28112          chLoading: s.consRematesLoading && !s.consRemates,
28113          chEmpty: !!s.consRemates && list.length === 0 && !s.consRematesLoading,
28114          chHasVideos: chVids.length > 0,
28115          chHasLibrary: libYears.length > 0,
28116          chLibCount: libItems.length + (libItems.length === 1 ?' grabación' : ' grabaciones'),
28117          chYearTabs: libYears.map((y) => ({
28118            label: String(y), go: () => this.setState({ channelYear: y }),
28119            bg: y === curYear ?col : 'transparent', color: y === curYear ?'#fff' : '#4a5c72',
28120            border: y === curYear ?col : '#d5e0ec', weight: y === curYear ?800 : 600,
28121          })),
28122          chLibrary: libItems.map(({ it }) => { const t = this.remateType(it); return {
28123            title: it.nombre_remate || 'Remate ' + name, thumb: channelOriginalThumb(it), thumbPosition: 'center',
28124            date: this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { day: 'numeric', month: 'long' }) : '',
28125            type: t[0].toUpperCase(), typeColor: t[1], open: () => this.openRemate(it),
28126          }; }),
28127          chVideos: chVids.map((v) => { const id = vidId(v); const live = String(v.liveBroadcastContent || v.status || '') === 'live' || v.isLive; return {
28128            title: v.title || 'Video del canal',
28129            href: v.url || (id ?'https://www.youtube.com/watch?v=' + id : this.histUrl({})),
28130            thumb: v.thumbnailUrl || v.thumbnail || (id ?'https://i.ytimg.com/vi/' + id + '/hqdefault.jpg' : 'assets/feria.png'),
28131            meta: v.publishedAt ?new Date(v.publishedAt).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }) : name,
28132            live: !!live,
28133          }; }),
28134          chStats: [
28135            { value: String(list.length), label: 'REMATES' },
28136            { value: String(nLive + nUp), label: 'AGENDADOS' },
28137            { value: String(nRec), label: 'GRABADOS' },
28138          ],
28139          chTabs: tabs.map((t) => ({
28140            label: t.label, count: t.n, live: !!t.live, go: () =>
28140 this.setState({ channelTab: t.key }),
28141            bg: t.key === tab ?col : 'rgba(255,255,255,0.75)', color: t.key === tab ?'#fff' : '#33465c',
28142            border: t.key === tab ?col : '#e2eaf3',
28143          })),
28144          chHasFeatured: !!feat,
28145          chFeat: feat ? (() => { const it = feat.it; const b = badgeFor(feat.mode); const t = this.remateType(it); return {
28146            title: it.nombre_remate || 'Remate ' + name, thumb: channelOriginalThumb(it), thumbPosition: 'center', date: fmtD(it),
28147            loc: it.ubicacion_remate || [it.localidad, it.provincia].filter(Boolean).join(', ') || '',
28148            badge: b.badge, badgeBg: b.badgeBg, live: b.live, type: t[0].toUpperCase(),
28149            open: () => this.openRemate(it),
28150          }; })() : {},
28151          chHasCards: grid.length > 0,
28152          chCards: grid.map(({ it, mode }) => { const b = badgeFor(mode); const t = this.remateType(it); return {
28153            title: it.nombre_remate || 'Remate ' + name, thumb: channelOriginalThumb(it), thumbPosition: 'center', date: fmtD(it),
28154            badge: b.badge, badgeBg: b.badgeBg, live: b.live, type: t[0].toUpperCase(), typeColor: t[1],
28155            open: () => this.openRemate(it),
28156          }; }),
28157        };
28158      })(),
28159      ...(() => {
28160        const month = this.homeFeaturedRemates(s.monthRemates, s.rosganRemates).map((it) => ({ it, mode: this.histMode(it) }))
28161          .sort((a, b) => this.remateDate(a.it) - this.remateDate(b.it));
28162        const badgeFor = (m) => m === 'live' ?{ badge: 'EN VIVO', badgeBg: '#e63946', badgeColor: '#fff', live: true } : m === 'upcoming' ?{ badge: 'PRÓXIMO', badgeBg: '#0a5ca8', badgeColor: '#fff', live: false } : { badge: 'FINALIZADO', badgeBg: 'rgba(0,29,61,0.82)', badgeColor: '#fff', live: false };
28163        const cafePlaylistId = 'PLAa_PiRkH0KOCs4VtYFA7NpUeuKs_rt4f';
28164        const cafeVids = (s.cafeVideos || []).slice(0, 10);
28165        const real = month.length;
28166        const loop = real > 1;
28167        this._monthReal = real; this._monthLoop = loop;
28168        const cs = Math.min(s.carSlide, Math.max(real - 1, 0));
28169        let rosganCardIndex = 0;
28170        const cards = month.map(({ it, mode }) => {
28171          const consRef = this.remateConsignatariaRef(it) || 'Rosgan';
28172          const consName = this.consDisplayName(this.consRecord(consRef)?.name || this.consRecord(consRef)?.nombre || it.nombre_cuenta || it.nombre_tipo || 'Rosgan');
28173          const isRosganCard = this.consKey(consRef) === 'rosgan' || this.consKey(consName) === 'rosgan';
28174          const anniversaryImage = this.rosganAnniversaryImage(it);
28175          const rosganCardBanner = isRosganCard && !anniversaryImage ?this.rosganBanner(rosganCardIndex++):null;
28176          const whiteConsLogo = this.consWhiteLogo(consRef);
28177          const consLogo = whiteConsLogo || this.consLogo(consRef);
28178          const brandColor = this.consBrandColor(consRef);
28179          return {
28180            cta: it.__feriaAuction && !it.url_streaming ?'Transmisión a confirmar' : 'Ver transmisión',
28181            title: it.nombre_remate || 'Remate Rosgan', select:(event)=>this.selectHomeChannel(it,event), href: this.histUrl(it), thumb: anniversaryImage || (rosganCardBanner ?rosganCardBanner.src : this.histThumb(it)),
28182            day: this.remateDate(it)?.toLocaleDateString('es-AR', { day: '2-digit', timeZone: 'America/Argentina/Buenos_Aires' }),
28183            month: this.remateDate(it)?.toLocaleDateString('es-AR', { month: 'short', timeZone: 'America/Argentina/Buenos_Aires' }),
28184            time: it.hora_remate ?String(it.hora_remate).slice(0, 5) + ' h' : 'Horario a confirmar',
28185            location: it.ubicacion_remate || 'Transmisión online',
28186            cons: consName, consLogo, hasConsLogo: !!consLogo, consLogoFilter: whiteConsLogo ?'none' : 'brightness(0) invert(1)', brandColor, thumbPosition: anniversaryImage ?'center 48%' : rosganCardBanner ?rosganCardBanner.position : this.consBackgroundPosition(consRef, 'card'),
28187            thumbOverlay: this.rgba(this.shade(brandColor, -0.06), 0.82),
28188            date: this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) + (it.hora_remate ?' · ' + String(it.hora_remate).slice(0, 5) : '') : '',
28189            ...badgeFor(mode), badge: mode === 'live' ?'EN VIVO' : this.homeAgendaDay(this.remateDate(it)) === this.homeAgendaDay(new Date()) ?'HOY' : 'PRÓXIMO',
28190          };
28191        });
28192        return {
28193          hasMonthCarousel: real > 0,
28194          monthHasDots: real > 1,
28195          monthDots: month.map((_, i) => ({
28196            go: () => { const el = document.querySelector('[data-drag-scroll]'); const card = el?.querySelector('[data-drag-item]'); const step = card ?card.offsetWidth + 16 : 300; const base = loop ?real * step : 0; el?.scrollTo({ left: base + i * step, behavior: 'smooth' }); this._carPause = true; setTimeout(() => { this._carPause = false; }, 6000); },
28197            w: i === cs ?'26px' : '6px', bg: i === cs ?'#002e5f' : '#cfdbe9',
28198          })),
28199          monthCarousel: loop ?[...cards, ...cards, ...cards] : cards,
28200          hasCafeVideos: cafeVids.length > 0,
28201          hasCafePlaylistFallback: s.cafeVideos !== null && cafeVids.length === 0,
28202          cafePlaylistHref: 'https://www.youtube.com/playlist?list=' + cafePlaylistId,
28203          cafePlaylistEmbed: 'https://www.youtube-nocookie.com/embed/videoseries?list=' + cafePlaylistId,
28204          cafePlaylistGrid: s.isMobile ?'1fr' : 'minmax(0, 1.5fr) minmax(250px, .7fr)',
28205          cafeCards: cafeVids.map((v) => {
28206            const vid = v.id || v.videoId || this.ytId(v.url);
28207            return { title: v.title || 'Café Ganadero', select:(event)=>this.selectHomeChannel(v,event), href: v.url || (vid ?'https://www.youtube.com/watch?v=' + vid : '#'),
28208              thumb: v.thumbnailUrl || (vid ?'https://i.ytimg.com/vi/' + vid + '/hqdefault.jpg' : 'assets/feria.png'),
28209              meta: v.publishedAt ?new Date(v.publishedAt).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Café Ganadero' };
28210          }),
28211        };
28212      })(),
28213      ...(() => {
28214        const canAdminRosganRemates = this.canManageUsers(s.apiUser);
28215        const form = s.rosganRemateForm || this.emptyRosganRemateForm();
28216        const consignatarias = (s.rosganLotConsignatarias || []).filter((item) => !this.isExcludedConsignataria(this.consName(item))).map((item) => ({
28217          value: String(item?.id || item?.consignatariaId || ''), label: this.consName(item), raw: item,
28218        })).filter((item) => item.value && item.label).sort((a, b) => a.label.localeCompare(b.label, 'es'));
28219        const consById = Object.fromEntries(consignatarias.map((item) => [item.value, item]));
28220        const assigned = this.rosganAssignedConsignataria(s.rosganLotConsignatarias || []);
28221        const activeTypes = s.rosganRemateTypes || [];
28222        const selectedType = this.rosganRemateType(form.tipoRemate, activeTypes);
28223        const typeAllowsAll = selectedType?.permiteModoTodos === true;
28224        const remates = this.rosganUpcomingRemates(s.rosganRemates).filter((item) => item.cargaDeLotes !== false && !this.isExcludedRemate(item));
28225        const activeCabanas = (s.cabanas || []).filter((item) => this.cabanaListingGroup(item) === 'active' && !this.isExcludedCabana(item));
28226        const dateLabel = (item) => {
28227          const date = item?.__cabanaAuction ?this.cabanaAuctionStartAt(item) : (item?.fechaHoraRemate ?new Date(item.fechaHoraRemate) : null);
28228          return date && !Number.isNaN(date.getTime())
28229            ?date.toLocaleDateString('es-AR', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' }) + ' · ' + date.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) + ' h'
28230            : 'Fecha pendiente';
28231        };
28232        const toggles = [
28233          ['canalRural', 'Canal Rural'], ['cargaDeLotes', 'Habilitar carga de lotes'],
28234          ['informaIndice', 'Informa índice'], ['mostrarComitentes', 'Mostrar c
28234omitentes'], ['closeRemate', 'Cerrar remate'],
28235        ].filter(([key]) => key !== 'closeRemate' || !!s.rosganRemateEditingId).map(([key, label]) => { const on = !!form[key]; return {
28236          label, toggle: () => this.toggleRosganRemateField(key), track: on ?'#16846e' : '#aebdca', knob: on ?'15px' : '0px',
28237          bg: on ?'#f0faf7' : '#f7f9fb', border: on ?'#b9dfd4' : '#dde6ef',
28238        }; });
28239        const message = s.rosganRemateMessage;
28240        return {
28241          showRosganRemates: true,
28242          canAdminRosganRemates,
28243          rematesIntro: canAdminRosganRemates ?'Creá y administrá los próximos remates y consultá el historial de Rosgan.' : 'Próximos remates e historial de Rosgan.',
28244          openRosganRemateCreate: () => this.chooseRemateCreationType(),
28245          rosganRematePageVisible: s.route === 'rosganRemateCreate' && s.rosganRemateFormOpen,
28246          showRemateArchive: s.route === 'remates' && !!s.remateArchiveOpen,
28247          showRemateArchiveToggle: s.route === 'remates',
28248          remateArchiveOpen: !!s.remateArchiveOpen,
28249          remateArchiveToggleLabel: s.remateArchiveOpen ?'Ocultar remates pasados' : 'Ver remates pasados',
28250          toggleRemateArchive: () => {
28251            const open = !this.state.remateArchiveOpen;
28252            this.setState({ remateArchiveOpen: open });
28253            if (open && !Array.isArray(this.state.remates) && !this.state.rematesLoading) this.fetchRemates(this.state.remateYear);
28254          },
28255          refreshRosganRemates: () => { this.loadRosganRemateAdmin(true); this.fetchCabanas(true); },
28256          rosganRematesLoading: s.rosganRematesLoading || s.cabanasLoading,
28257          rosganRematesInitialLoading: (!!s.rosganRematesLoading && !Array.isArray(s.rosganRemates)) || (!!s.cabanasLoading && !Array.isArray(s.cabanas)),
28258          rosganRematesHasError: !!(s.rosganRematesError || s.cabanasError) && remates.length + activeCabanas.length === 0,
28259          rosganRematesError: [s.rosganRematesError, s.cabanasError].filter(
28259Boolean).join(' · '),
28260          hasRosganRemates: remates.length + activeCabanas.length > 0,
28261          rosganRematesEmpty: !s.rosganRematesLoading && !s.cabanasLoading && !s.rosganRematesError && !s.cabanasError && s.rosganRemates !== null && s.cabanasCollectionReady && remates.length + activeCabanas.length === 0,
28262          rosganRemateCards: [
28263            ...remates.map((item) => ({ item, date: item?.fechaHoraRemate ?new Date(item.fechaHoraRemate) : null })),
28264            ...activeCabanas.map((item) => ({ item: { ...item, __cabanaAuction: true }, date: this.cabanaAuctionStartAt(item) })),
28265          ].sort((a, b) => (a.date || 0) - (b.date || 0)).map(({ item }) => {
28266            if (item.__cabanaAuction) {
28267              const participant = this.cabanaRemateParticipants(item)[0] || {};
28268              const mode = this.cabanaMode(item);
28269              const consignataria = this.cabanaConsignatariaName(item, rawConsignatarias);
28270              const consignatariaLogo = this.consWhiteLogo(consignataria) || this.consLogo(consignataria);
28271              const logo = consignatariaLogo;
28272              return {
28273                ...this.remateListCatalog(item, 'cabana'), prebidsActive: window.CabanaPrebid.isOpen(item),
28274                title: item.nombre_remate || item.title || item.titulo || 'Remate de cabaña', date: dateLabel(item),
28275                showFact: true, factLabel: 'CABAÑA', factValue: participant.name || 'Cabaña principal',
28276                consignataria, showConsignataria: false, consignatariaLogo, consignatariaLogoBg: this.consBrandColor(consignataria), hasConsignatariaLogo: !!consignatariaLogo, showHeaderConsignatariaLogo: false,
28277                location: item.ubicacion_remate || item.lugar || item.localidad || item.provincia || item?.location?.lugar || item?.location?.localidad || 'Pendiente',
28278                typeDetail: 'Reproductores', ...this.remateListPlace(item), status: mode === 'live' ?'EN VIVO · CABAÑAS' : 'ACTIVO · CABAÑAS', logo, hasLogo: !!logo, noLogo: !logo,
28279                logoBg: this.consWhiteLogo(consignataria) ?this.consBrandColor(consignataria) : '#fff', initial: consignataria?.trim().charAt(0).toUpperCase() || 'R',
28280                open: () => this.openCabana(item),
28281              };
28282            }
28283            const id = String(item?.id || '');
28284            const cons = consById[String(item?.consignatariaId || '')];
28285            const consName = item?.consignatariaNombre || cons?.label || 'Consignataria pendiente';
28286            const whiteLogo = this.consWhiteLogo(consName);
28287            const logo = whiteLogo || this.consLogo(consName);
28288            return {
28289              ...this.remateListCatalog(item),
28290              title: item?.titulo || 'Remate Rosgan', date: dateLabel(item), showFact: Number(item?.cabezas) > 0, factLabel: 'CABEZAS', factValue: Number(item?.cabezas) > 0 ? Number(item.cabezas).toLocaleString('es-AR') : '',
28291              location: [item?.localidad, item?.provincia].filter(Boolean).join(', ') || item?.lugar || 'Pendiente', consignataria: consName,
28292              typeDetail: 'Invernada, Faena y Cría', ...this.remateListPlace(item), status: item?.closeRemate ?'CERRADO' : item?.activo === false ?'INACTIVO' : 'ACTIVO', logo, hasLogo: !!logo, noLogo: !logo,
28293              logoBg: whiteLogo ?this.consBrandColor(consName) : '#eef6fd', initial: consName.trim().charAt(0).toUpperCase() || 'R',
28294              open: () => this.openRosganActiveRemate(item),
28295            };
28296          }),
28297          rosganRemateFormOpen: s.rosganRemateFormOpen,
28298          rosganRemateFormEyebrow: s.rosganRemateEditingId ?'EDITAR REMATE' : 'NUEVO REMATE',
28299          rosganRemateFormTitle: s.rosganRemateEditingId ?'Editar remate Rosgan' : 'Crear remate Rosgan',
28300          closeRosganRemateForm: () => this.closeRosganRemateForm(),
28301          saveRosganRemate: () => this.saveRosganRemate(), rosganRemateSaving: s.rosganRemateSaving,
28302          rosganRemateSaveLabel: s.rosganRemateSaving ?'Guardando…' : (s.rosganRemateEditingId ?'Guardar cambios' : 'Crear remate'),
28303          rrfTitle: form.titulo, setRrfTitle: (e) => this.setRosganRemateField('titulo', e.target.value),
28304          rrfDate: form.fecha, setRrfDate: (e) => this.setRosganRemateField('fecha', e.target.value),
28305          rrfTime: form.hora, setRrfTime: (e) => this.setRosganRemateField('hora', e.target.value),
28306          rrfHeads: form.cabezas, setRrfHeads: (e) => this.setRosganRemateField('cabezas', e.target.value),
28307          rrfType: form.tipoRemate, setRrfType: (e) => this.setRosganRemateField('tipoRemate', e.target.value),
28308          rosganRemateTypeOptions: activeTypes.map((item) => ({ value: item.id, label: item.name })),
28309          rrfTypeDisabled: s.rosganRemateTypesLoading || !activeTypes.length,
28310          rrfTypePlaceholder: s.rosganRemateTypesLoading ?'Cargando tipos…' : 'Seleccionar tipo…',
28311          rrfTypesMessageVisible: !!s.rosganRemateTypesError,
28312          rrfTypesMessage: s.rosganRemateTypesError || '',
28313          rrfMode: form.modoConsignataria === 'Todos' && typeAllowsAll ?'Todos' : 'Indiv
28313idual',
28314          setRrfMode: (e) => this.setRosganRemateField('modoConsignataria', e.target.value),
28315          rosganRemateModeOptions: typeAllowsAll ?[{ value: 'Individual', label: 'Individual' }, { value: 'Todos', label: 'Todos' }] : [{ value: 'Individual', label: 'Individual' }],
28316          rrfModeLocked: !typeAllowsAll,
28317          rrfShowConsignataria: form.modoConsignataria !== 'Todos' || !typeAllowsAll,
28318          rrfAllConsignatarias: form.modoConsignataria === 'Todos' && typeAllowsAll,
28319          rrfConsignataria: form.consignatariaId, setRrfConsignataria: (e) => this.setRosganRemateField('consignatariaId', e.target.value),
28320          rrfConsignatariaLocked: !this.canManageUsers(s.apiUser),
28321          rosganRemateConsignatarias: this.canManageUsers(s.apiUser) ?consignatarias : (assigned ?consignatarias.filter((item) => item.value === String(assigned.id)) : []),
28322          rrfProvince: form.provincia, setRrfProvince: (e) => this.setRosganRemateField('provincia', e.target.value),
28323          rosganRemateProvinces: s.geoProvinces || [],
28324          rrfLocality: form.localidad, setRrfLocality: (e) => this.onRosganRematePlaceInput(e.target.value),
28325          rrfLocalityDisabled: !form.provincia,
28326          rrfLocalityPlaceholder: !form.provincia ?'Elegí una provincia primero' : 'Escribí al menos 3 letras',
28327          rrfPlacesExpanded: String(!!s.rrfPlaceOpen && !!s.rrfPlaceResults?.length),
28328          rrfPlacesVisible: !!s.rrfPlaceOpen && !!s.rrfPlaceResults?.length,
28329          rrfPlaceActiveId: s.rrfPlaceIndex >= 0 ? 'rrf-place-' + s.rrfPlaceIndex : '',
28330          rrfPlaceStatus: s.rrfPlaceLoading ? 'Buscando localidades…' : s.rrfPlaceError || '',
28331          rrfPlaces: (s.rrfPlaceResults || []).map((item,index)=>({...item,id:'rrf-place-'+index,source:item.sourceLabel || (item.source === 'google'?'Google Places':'Localidad argentina'),selected:String(index === s.rrfPlaceIndex),bg:index === s.rrfPlaceIndex?'#edf5fc':'#fff',pickPointer:event=>{event.preventDefault();this.selectRosganRematePlace(item);},pick:()=>this.selectRosganRematePlace(item)})),
28332          rrfPlaceKey: event => this.rosganRematePlaceKey(event),
28333          rrfPlaceMouseDown: event => event.preventDefault(),
28334          rrfPlaceFocus: () => { clearTimeout(this._rrfPlaceBlur); if (s.rrfPlaceResults?.length) this.setState({rrfPlaceOpen:true}); },
28335          rrfPlaceBlur: () => { this._rrfPlaceBlur=setTimeout(()=>this.resetRosganRematePlaces(),150); },
28336          rosganRemateLocalities: (s.geoLocs?.[form.provincia] || []).map((item) => item.nombre),
28337          rrfPlace: form.lugar, setRrfPlace: (e) => this.setRosganRemateField('lugar', e.target.value),
28338          rosganRemateToggles: toggles,
28339          setRosganRemateFlyer: (e) => this.setRosganRemateFlyer(e.target.files?.[0] || null),
28340          rrfHasFlyer: !!s.rosganRemateFlyerPreview, rrfNoFlyer: !s.rosganRemateFlyerPreview, rrfFlyerPreview: s.rosganRemateFlyerPreview,
28341          rosganRemateHasMessage: !!message, rosganRemateMessageText: message?.text || '',
28342          rosganRemateMessageBg: message?.ok ?'#effaf6' : '#fff4f4', rosganRemateMessageBorder: message?.ok ?'#b9e2d4' : '#efc4c8', rosganRemateMessageColor: message?.ok ?'#176b58' : '#a32934',
28343        };
28344      })(),
28345      ...(() => {
28346        const all = this.unifiedArchiveRemates(s.remates, s.rosganRemates, s.cabanas, s.remateYear);
28347        const withType = all.map((r) => ({ ...r, _t: r.__cabanaAuction ?this.REMATE_TYPES.cabanas : this.remateType(r) }));
28348        const counts = {};
28349        withType.forEach((r) => { const k = r._t[0]; counts[k] = (counts[k] || 0) + 1; });
28350        const filtered = s.remateType ?withType.filter((r) => r._t[0] === s.remateType) : withType;
28351        const itemDate = (item) => item.__cabanaAuction ?this.cabanaAuctionStartAt(item) : this.remateDate(item);
28352        const sorted = [...filtered].sort((a, b) => (itemDate(b) || 0) - (itemDate(a) || 0));
28353        const typeKeys = [...new Set(withType.map((r) => r._t[0]))];
28354        const yearNow = new Date().getFullYear();
28355        const dateLabel = (r) => { const d = itemDate(r); if (!d) return 'Fecha pendiente'; const dl = d.toLocaleDateString('es-AR', { day: '2-digit', month: 'short', year: 'numeric' }); const t = String(r.hora_remate || r.hora || r?.schedule?.hora || '').slice(0, 5); return t ?dl + ' · ' + t + ' h' : dl; };
28356        const mapCard = (r) => {
28357            if (r.__cabanaAuction) {
28358              const participant = this.cabanaRemateParticipants(r)[0] || {};
28359              const color = this.cabanaColor(r);
28360              const breed = this.cabanaBreedOf(r) || 'Reproductores';
28361              const consignataria = this.cabanaConsignatariaName(r, rawConsignatarias);
28362              const consignatariaLogo = this.consWhiteLogo(consignataria) || this.consLogo(consignataria);
28363              const logo = consignatariaLogo;
28364              return {
28365                ...this.remateListCatalog(r, 'cabana'), prebidsActive: window.CabanaPrebid.isOpen(r),
28366                open: () =>
28366 this.openCabana(r), color, type: 'CABAÑAS', typeDetail: 'Reproductores', initial: consignataria?.trim().charAt(0).toUpperCase() || 'R',
28367                logo, hasLogo: !!logo, noLogo: !logo, logoBg: this.consWhiteLogo(consignataria) ?this.consBrandColor(consignataria) : '#fff', logoBorder: 'transparent',
28368                title: r.nombre_remate || r.title || r.titulo || 'Remate de cabaña', date: dateLabel(r),
28369                location: r.ubicacion_remate || r.lugar || r.localidad || r.provincia || r?.location?.lugar || r?.location?.localidad || 'Localidad pendiente',
28370                ...this.remateListPlace(r), metaLabel: 'CABAÑA · ' + breed.toUpperCase(), metaValue: participant.name || 'Cabaña principal', showMeta: true,
28371                consignataria, showConsignataria: false, consignatariaLogo, consignatariaLogoBg: this.consBrandColor(consignataria), hasConsignatariaLogo: !!consignatariaLogo, showHeaderConsignatariaLogo: false,
28372              };
28373            }
28374            const consignataria = this.consDisplayName(r.nombre_cuenta || r.nombre_consignatario) || 'Rosgan';
28375            const whiteLogo = this.consWhiteLogo(consignataria);
28376            const logo = whiteLogo || this.consLogo(consignataria);
28377            const consignatariaLogo = this.consWhiteLogo(consignataria) || this.consLogo(consignataria);
28378            return {
28379              ...this.remateListCatalog(r, r.__rosganClosedArchive || r.__rosganScheduled ? 'active' : 'historical'),
28380              open: () => r.__rosganClosedArchive || r.__rosganScheduled ?this.openRosganActiveRemate(r) : this.openRemate(r),
28381              color: r._t[1], type: r._t[0].toUpperCase(), typeDetail: /feria/i.test(r._t[0]) ? 'Físico' : /cabaña/i.test(r._t[0]) ? 'Reproductores' : 'Invernada, Faena y Cría', initial: consignataria.trim()[0].toUpperCase(),
28382              logo, hasLogo: !!logo, noLogo: !logo,
28383              logoBg: whiteLogo ?this.consBrandColor(consignataria) : '#f4f7fb', logoBorder: whiteLogo ?'transparent' : '#e6edf5',
28384              title: r.nombre_remate || 'Remate Rosgan', date: dateLabel(r),
28385              location: r.ubicacion_remate || [r.localidad, r.provincia].filter(Boolean).join(', ') || 'Localidad pendiente',
28386              ...this.remateListPlace(r), metaLabel: 'CONSIGNATARIA', metaValue: consignataria, showMeta: this.consKey(consignataria) !== 'rosgan',
28387              consignatariaLogo, consignatariaLogoBg: this.consBrandColor(consignataria), hasConsignatariaLogo: !!consignatariaLogo,
28388            };
28389        };
28390        const scheduledCabanas = (s.cabanas || []).filter((item) => this.cabanaListingGroup(item) === 'upcoming' && !this.isExcludedCabana(item))
28391          .map((item) => ({ ...item, __cabanaAuction: true, _t: this.REMATE_TYPES.cabanas }));
28392        const scheduled = [...this.rosganScheduledRemates(s.monthRemates, s.rosganRemates).map((r) => ({ ...r, _t: this.remateType(r) })), ...scheduledCabanas]
28393          .sort((a,b) => (itemDate(a)?.getTime() || Infinity) - (itemDate(b)?.getTime() || Infinity));
28394        const latestFinished = this.unifiedLatestFinishedRemates(s.latestFinished, s.rosganRemates, s.cabanas).map((r) => ({ ...r, _t: r.__cabanaAuction ?this.REMATE_TYPES.cabanas : this.remateType(r) }));
28395        return {
28396          scheduledRemateCards: scheduled.map(mapCard),
28397          latestFinishedCards: latestFinished.map(mapCard),
28398          hasLatestFinished: latestFinished.length > 0,
28399          latestFinishedLoading: !!s.latestFinishedLoading || !!s.cabanasLoading,
28400          latestFinishedError: [s.latestFinishedError, s.cabanasError].filter(
28400Boolean).join(' · '),
28401          retryLatestFinished: () => { this.fetchLatestFinishedRemates(); this.fetchCabanas(true); },
28402          hasScheduledRemates: scheduled.length > 0,
28403          scheduledRematesLoading: s.monthRemates === null,
28404          remateArchiveHasMore: sorted.length > s.remateArchiveLimit,
28405          moreRemateArchive: () => this.setState({ remateArchiveLimit: this.state.remateArchiveLimit + 12 }),
28406          remateYear: String(s.remateYear), onRemateYear: (e) => { const y = Number(e.target.value); this.setState({ remateYear: y, remates: null, remateType: '', remateArchiveLimit: 12 }); this.fetchRemates(y); },
28407          remateYears: (s.remateYears && s.remateYears.length ?s.remateYears : Array.from({ length: 8 }, (_, i) => String(yearNow - i))),
28408          remateTypeChips: [{ label: 'Todos', key: '', dot: '#002e5f', count: withType.length }, ...typeKeys.map((k) => ({ label: k, key: k, dot: (Object.values(this.REMATE_TYPES).find((v) => v[0] === k) || ['', '#002e5f'])[1], count: counts[k] }))].map((c) => ({
28409            label: c.label, count: c.count, dot: c.dot, go: () => this.setState({ remateType: c.key }),
28410            bg: s.remateType === c.key ?'#002e5f' : '#fff', color: s.remateType === c.key ?'#fff' : '#3d4c5f', border: s.remateType === c.key ?'#002e5f' : '#e3eaf2',
28411          })),
28412          rematesLoadingVisible: s.rematesLoading || s.remates === null || s.cabanasLoading,
28413          rematesReady: !s.rematesLoading && s.remates !== null && !s.cabanasLoading,
28414          remateFiltersVisible: withType.length > 0,
28415          hasRemates: sorted.length > 0, rematesEmpty: !s.rematesLoading && !!s.remates && sorted.length === 0,
28416          remateCards: s.remateArchiveOpen ?sorted.slice(0, s.remateArchiveLimit).map(mapCard) : [],
28417        };
28418      })(),
28419      ...(() => {
28420        const rm = s.currentRemate || {};
28421        const t = this.remateType(rm);
28422        const raw = s.route === 'remateDetail' ? this.publicRemateLots(s.remateLots, rm) : (s.remateLots || []);
28423        const orderDefined = !rm.__rosganActive || (s.catalogPublication?.key === this.catalogPublicationKey(rm) && s.catalogPublication.salesOrderDefined === true);
28424        const lotNumber = (l) => rm.__rosganActive
28425          ?(orderDefined ? l.lotNumber : null)
28426          :(l.orden_final ?? l.orden_venta ?? l.lotNumber);
28427        const lotNum = (l) => { const n = Number(lotNumber(l)); return Number.isFinite(n) && n > 0 ?'Lote ' + n : ''; };
28428        const heads = (l) => Number(l.cantidad) || 0;
28429        const wt = (l) => Number(l.peso_promedio) || 0;
28430        const loc = (l) => [l.nombre_localidad, l.nombre_provincia].filter(Boolean).join(', ');
28431        const q = s.rdQuery.trim().toLowerCase();
28432        let list = raw.filter((l) => !q || [lotNum(l), l.nro_certificado, l.nombre_categoria, l.raza, loc(l)].join(' ').toLowerCase().includes(q));
28433        const filterId=this.remateRouteId(rm);
28434        const selectedFilters=s.rdLotFilters?.[filterId]||{};
28435        const filterGroups=window.RemateFilters.facets(list,selectedFilters).filter(group=>group.alwaysVisible || group.options.length>1 || group.options.some(option=>option.active));
28436        const toggleCategory = value => {
28437          const latest = this.state.rdLotFilters?.[filterId] || {};
28438          const values = latest.category || [];
28439          this.setState({ rdLotFilters: { ...this.state.rdLotFilters, [filterId]: { ...latest, category: values.includes(value) ?values.filter(item=>item!==value) : [...values,value] } } });
28440        };
28441        list=window.RemateFilters.filterLots(list,selectedFilters);
28442        const filterCount=Object.values(selectedFilters).reduce((n,v)=>n+v.length,0);
28443        if (s.rdSort === 'peso-desc') list = [...list].sort((a, b) => wt(b) - wt(a));
28444        if (s.rdSort === 'peso-asc') list = [...list].sort((a, b) => wt(a) - wt(b));
28445        if (s.rdSort === 'cabezas') list = [...list].sort((a, b) => heads(b) - heads(a));
28446        if (rm.__rosganActive || s.rdSort === 'orden') list = [...list].sort((a, b) => this.compareRosganSalesOrder(a,b));
28447        const totalHeads = rm.__rosganActive ? (s.route === 'remateDetail' ? this.publicRemateHeads(raw, rm) : this.rosganRemateHeads(rm, raw)) : raw.reduce((a, l) => a + heads(l), 0);
28448        const categoryHeads = Object.values(raw.reduce((groups, lot) => {
28449          const label = String(lot.nombre_categoria || 'Sin categoría').trim() || 'Sin categoría';
28450          const key = label.toLocaleLowerCase('es-AR');
28451          if (!groups[key]) groups[key] = { label, heads: 0 };
28452          groups[key].heads += heads(lot);
28453          return groups;
28454        }, {})).filter((item) => item.heads > 0).sort((a, b) => b.heads - a.heads || a.label.localeCompare(b.label, 'es'));
28455        const rosganOrderLots = Array.isArray(s.rosganSalesOrderLots) ?s.rosganSalesOrderLots : [];
28456        const orderSchedule = this.normalizeRosganSalesOrderSchedule(s.rosganSalesOrderSchedule);
28457        const scheduleByCert = new Map(orderSchedule.entries.map((entry) => [entry.cert, entry]));
28458        const orderCategoryOf = (lot) => String(lot.nombre_categoria || lot.categoryName || 'Sin categoría').trim() || 'Sin categoría';
28459        const orderConsignatariaOf = (lot) => this.consDisplayName(lot.nombre_consignatario || lot.consignatariaNombre || rm.nombre_cuenta || rm.nombre_consignatario) || 'Sin consignataria';
28460        const orderStatusLabels = { cargado: 'Cargado', controlado: 'Controlado', publicado: 'Publicado', anulado: 'Anulado', cerrado: 'Cerrado', 'sin-estado': 'Sin estado' };
28461        const orderQuery = this.consKey(s.rosganSalesOrderQuery || '');
28462        const orderStatusFilter = String(s.rosganSalesOrderStatusFilter || '');
28463        const orderCategoryFilter = String(s.rosganSalesOrderCategoryFilter || '');
28464        const orderConsignatariaFilter = String(s.rosganSalesOrderConsignatariaFilter || '');
28465        const orderRatingFilter = String(s.rosganSalesOrderRatingFilter || '');
28466
28467        const ratingColors = ['#c7353f', '#e05a32', '#e58a19', '#b5a20b', '#56a044', '#16846e'];
28468        const rosganOrderRows = rosganOrderLots.map((lot, index) => {
28469          const cert = this.remateLotRouteId(lot);
28470          const scheduleEntry = scheduleByCert.get(cert);
28471          const category = orderCategoryOf(lot);
28472          const workflowStatus = this.rosganLotWorkflowStatus(lot);
28473          const rating = Number(s.rosganSalesOrderRatings?.[cert]) || 0;
28474          const ratingColor = rating ?ratingColors[rating - 1] : '#8a9aad';
28475          const fallbackMedia = this.rosganSalesOrderMedia(lot);
28476          const videoUrl = lot.salesVideoUrl || fallbackMedia.url;
28477          const videoKind = lot.salesVideoKind || fallbackMedia.kind;
28478          const videoLabel = lot.salesVideoLabel || fallbackMedia.label;
28479          const consignataria = orderConsignatariaOf(lot);
28480          const whiteConsignatariaLogo = this.consWhiteLogo(consignataria);
28481          const consignatariaLogo = whiteConsignatariaLogo || this.consLogo(consignataria);
28482          const locality = [lot.nombre_localidad || lot.locality || lot.establishmentLocality, lot.nombre_provincia || lot.province || lot.establishmentProvince].filter(Boolean).join(', ');
28483          const paymentTerm = lot.plazo || lot.paymentTerm || lot.commercial?.paymentTerm || lot.sale?.paymentTerm || '';
28484          const quantity = Number(lot.cantidad) || 0;
28485          const weight = Number(lot.peso_promedio) || 0;
28486          return {
28487            cert, position: index + 1, maxPosition: rosganOrderLots.length,
28488            title: 'Cert. ' + cert,
28489            isOutOfCatalog: lot.isOutOfCatalog === true,
28490            category,
28491            breed: lot.raza || '', consignataria,
28492            statusKey: workflowStatus,
28493            statusLabel: orderStatusLabels[workflowStatus] || String(workflowStatus).replace(/(^|-)(\w)/g, (_, separator, letter) => (separator ?' ' : '') + letter.toUpperCase()),
28494            statusClass: ['cargado', 'controlado', 'publicado', 'anulado'].includes(workflowStatus) ?'is-' + workflowStatus : 'is-otro',
28495            searchText: this.consKey([index + 1, cert, category, lot.raza, consignataria, locality, paymentTerm, scheduleEntry?.alias, orderStatusLabels[workflowStatus] || workflowStatus].filter(Boolean).join(' ')),
28496            consignatariaLogo, hasConsignatariaLogo: !!consignatariaLogo, noConsignatariaLogo: !consignatariaLogo,
28497            consignatariaLogoBg: whiteConsignatariaLogo ?this.consBrandColor(consignataria) : (consignatariaLogo ?'#fff' : this.consBrandColor(consignataria)),
28498            consignatariaInitial: String(consignataria).trim().charAt(0).toUpperCase() || 'R',
28499            facts: [
28500              { label: 'CABEZAS', value: quantity ?quantity.toLocaleStr
28500ing('es-AR') : '—' },
28501              { label: 'PESO PROM.', value: weight ?weight.toLocaleString('es-AR') + ' kg' : '—' },
28502              { label: 'LOCALIDAD', value: locality || '—' },
28503              { label: 'PLAZO', value: paymentTerm || '—' },
28504            ],
28505            hasExcelSchedule: !!scheduleEntry,
28506            excelAlias: scheduleEntry?.alias || '',
28507            estimatedTime: window.RosganSalesOrderImport.clock(scheduleEntry?.estimatedStartSeconds),
28508            averageTime: window.RosganSalesOrderImport.duration(scheduleEntry?.averageSeconds) + ' min',
28509            filterRating: Number((s.rosganSalesOrderRatingSnapshot || s.rosganSalesOrderRatings)?.[cert]) || 0,
28510            rating, ratingLabel: rating ?'Nivel ' + rating + ' de 6' : 'Sin puntaje', ratingColor,
28511            ratingStars: Array.from({ length: 6 }, (_, star) => ({
28512              value: star + 1, active: star < rating, color: star < rating ?ratingColor : '#c8d3df',
28513              pick: () => this.setRosganSalesOrderRating(lot, star + 1),
28514              label: 'Puntuar ' + (star + 1) + ' de 6 el lote ' + cert,
28515            })),
28516            videoUrl, hasVideo: !!videoUrl, noVideo: !videoUrl, videoLabel: videoLabel || 'VIDEO', videoIsRaw: videoKind === 'raw',
28517            videoBadgeClass: videoKind === 'raw' ?'is-raw' : 'is-final', videoOpen: s.rosganSalesOrderVideoCert === cert,
28518            videoButtonLabel: s.rosganSalesOrderVideoCert === cert ?'Cerrar video' : 'Ver video ' + (videoLabel || ''),
28519            videoPoster: lot.thumbnailUrl || lot.thumbnail || lot.imagen || '', toggleVideo: () => this.toggleRosganSalesOrderVideo(lot),
28520            playVideo: (event) => this.playRosganSalesOrderVideo(lot, event),
28521            videoDidPlay: (event) => this.markRosganSalesOrderVideoPlaying(event),
28522            videoDidPause: (event) => this.markRosganSalesOrderVideoPaused(event),
28523            videoDidError: (event) => this.markRosganSalesOrderVideoError(event),
28524            isFirst: index === 0, isLast: index === rosganOrderLots.length - 1,
28525            moveUp: () => this.moveRosganSalesOrder(index, -1), moveDown: () => this.moveRosganSalesOrder(index, 1),
28526            setPosition: (event) => this.setRosganSalesOrderPosition(index, event),
28527          };
28528        }).filter((row) => (!orderQuery || row.searchText.includes(orderQuery))
28529          && (!orderStatusFilter || row.statusKey === orderStatusFilter)
28530          && (!orderCategoryFilter || row.category === orderCategoryFilter)
28531          && (!orderConsignatariaFilter || row.consignataria === orderConsignatariaFilter)
28532          && (orderRatingFilter === 'unrated' ? !row.filterRating : orderRatingFilter === 'rated' ? !!row.filterRating : true));
28533        const dd = this.remateDate(rm);
28534        const lb = s.route === 'remateDetail' && rm.__rosganActive && !this.isRosganLotPublished(s.rdLightbox) ? null : s.rdLightbox;
28535        const lbNeighbors = this.remateLotNeighbors(lb, raw);
28536        const isActiveRosgan = !!rm.__rosganActive;
28537        const remateId = this.remateRouteId(rm);
28538        const lotCert = this.remateLotRouteId(lb);
28539      const videoKey = isActiveRosgan && lotCert ?this.rosganLotVideoKey(remateId, lotCert) : '';
28540      const videoState = videoKey ?(s.rosganLotVideoState?.[videoKey] || { loading: true, videos: [], uploads: [], error: '' }) : { loading: false, videos: [], uploads: [], error: '' };
28541      const loadedPrimaryVideo = (videoState.videos || []).find((video) => video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl);
28542      const lbPrimaryVideoUrl = lb?.videoUrl || lb?.video_uri || loadedPrimaryVideo?.url || loadedPrimaryVideo?.publicUrl || loadedPrimaryVideo?.videoUrl || loadedPrimaryVideo?.downloadUrl || loadedPrimaryVideo?.signedUrl || '';
28543        const canManageVideos = isActiveRosgan && this.canEditRosganLot(lb);
28544        const videoDeletingPrefix = videoKey + ':';
28545        const hasLotValue = (value) => {
28546          if (value === null || value === undefined) return false;
28547          const normalized = String(value).trim().toLowerCase();
28548          return !['', '0', '?', '-', '—', 'n/a', 'null', 'undefined', 'sin dato', 's/d'].includes(normalized);
28549        };
28550        const factsOf = (l) => [
28551          { label: 'CABEZAS', value: heads(l) || '' }, { label: 'PESO PROM.', value: wt(l) ?wt(l) + ' kg' : '' },
28552          { label: 'PLAZO', value: l.plazo || '' }, { label: 'LOCALIDAD', value: loc(l) },
28553        ].filter((item) => hasLotValue(item.value));
28554        const rosganFieldIconSlots = {
28555          'CABEZAS':1, 'MACHOS':2, 'HEMBRAS':3, 'MADRES':4, 'CRÍAS':5,
28556          'PESO PROM.':6, 'PESO MÍNIMO':7, 'PESO MÁXIMO':8,
28557          'CATEGORÍA':9, 'RAZA':10, 'TIPO':11, 'CLASE':12, 'EDAD':13,
28558          'ESTADO':14, 'DESCRIPCIÓN':15, 'DENTICIÓN':16,
28559          'ESTADO REPRODUCTIVO':17, 'DESTINO':18, 'PLAZO':19, 'PESAJE':20,
28560          'DESBASTE':21, 'ALIMENTACIÓN':22, 'SANIDAD':23, 'GARRAPATA':24,
28561          'MÍO MÍO':25, 'APTO CHINA':26, 'TÉRMINO':27, 'MARCA LÍQUIDA':28,
28562          'APTO VISEC':29, 'SERVICIO':30, 'PREÑEZ':31, 'PARICIÓN':32,
28563          'SUBCATEGORÍA':33, 'CONDICIÓN':34, 'ENTREGA':35,
28564          'TERMINACIÓN':36, 'RINDE ESTIMADO':37,
28565        };
28566        const lotCards = (items) => items.filter((item) => hasLotValue(item.value)).map((item) => {
28567          const index = (rosganFieldIconSlots[item.label] || 15) - 1;
28568          return {...item, isBreed:item.label === 'RAZA', iconX:(index % 8) * 100 / 7, iconY:Math.floor(index / 8) * 25};
28569        });
28570        const kiloValue = (value) => hasLotValue(value) ?Number(value).toLocaleString('es-AR') + ' kg' : '';
28571        const headValue = (value) => hasLotValue(value) ?Number(value).toLocaleString('es-AR') : '';
28572        const booleanValue = (value) => typeof value === 'boolean' ?(value ?'Sí' : 'No') : '';
28573        const formTypeValue = (value) => ({ invernada: 'Invernada', vientres: 'Vientres', faena: 'Faena' }[String(value || '').toLowerCase()] || value || '');
28574        const rdOwnCompany=s.userConsignataria || s.userConsignatarias?.[0];
28575        const rdOwnCompanyRef=String(rdOwnCompany?.id || rdOwnCompany?.ref || rdOwnCompany?.name || '');
28576        const rdOwnCompanyRecord=this.consRecord(rdOwnCompanyRef);
28577        const rdOwnCompanyName=rdOwnCompanyRef?(rdOwnCompanyRecord?.name || rdOwnCompanyRecord?.nombre || rdOwnCompanyRef.replace(/-s-r-l$/i,' S.R.L.').replace(/-s-a$/i,' S.A.').replace(/-/g,' ').replace(/\b\p{L}/gu,c=>c.toUpperCase())):'';
28578        const rdConsignataria = this.consDisplayName(rm.nombre_cuenta || rm.nombre_consignatario) || 'Rosgan';
28579        const lbConsignataria = this.consDisplayName(lb?.nombre_consignatario || rdConsignataria) || 'Rosgan';
28580        const lbConsignatariaLogo = this.consWhiteLogo(lbConsignataria) || this.consLogo(lbConsignataria);
28581        const lbConsignatariaColor = this.consBrandColor(lbConsignataria);
28582        const lbRepresentative = this.rosganLotRepresentative(lb);
28583        const lbRepresentativeAvatar = this.commercialAvatar(lbRepresentative, lbConsignataria, !!s.rosganRepresentativePhotoFailed?.[lbRepresentative?.uid]);
28584        const lbBreedIcons = this.rosganBreedIcons(lb?.raza);
28585        const descriptionParts = (Array.isArray(lb?.descripcion_lote) ? lb.descripcion_lote : [lb?.descripcion_lote || '']).flatMap(value => String(value).split(/\s*[·;\n]\s*/));
28586        const descriptionAges = descriptionParts.map(value => value.match(/^Edad\s*:\s*(.+)$/i)?.[1]).filter(Boolean);
28587        const lbDescription = descriptionParts.filter(value => !/^Edad\s*:/i.test(value)).join(' · ');
28588        const lbAge = lb?.edad || [...new Set(descriptionAges)].join(' · ');
28589        const lbObservations = [lb?.observations, lb?.observaciones_certificado, lb?.observaciones].map(value => String(value ?? '').trim()).find(Boolean) || '';
28590        const lbDetailGroups = lb ?[
28591          { label: 'CANTIDAD Y PESO', cards: lotCards([
28592            { label: 'CABEZAS', value: headValue(lb.cantidad) }, { label: 'MACHOS', value: headValue(lb.cantidad_machos) },
28593            { label: 'HEMBRAS', value: headValue(lb.cantidad_hembras) }, { label: 'MADRES', value: headValue(lb.cantidad_madres) },
28594            { label: 'CRÍAS', value: headValue(lb.cantidad_crias) }, { label: 'PESO PROM.', value: kiloValue(lb.peso_promedio) },
28595            { label: 'PESO MÍNIMO', value: kiloValue(lb.peso_minimo) }, { label: 'PESO MÁXIMO', value: kiloValue(lb.peso_maximo) },
28596          ]) },
28597          { label: 'CARACTERÍSTICAS DEL LOTE', cards: lotCards([
28598            { label: 'CATEGORÍA', value: lb.nombre_categoria }, { label: 'RAZA', value: lb.raza },
28599            { label: 'CLASE', value: lb.clase },
28600            { label: 'EDAD', value: lbAge }, { label: 'ESTADO', value: lb.estado },
28601            { label: 'DESCRIPCIÓN', value: lbDescription }, { label: 'DENTICIÓN', value: lb.denticion },
28602            { label: 'ESTADO REPRODUCTIVO', value: lb.estado_reproductivo }, { label: 'DESTINO', value: lb.destino },
28603          ]) },
28604          { label: 'CONDICIONES', cards: lotCards([
28605            { label: 'PLAZO', value: lb.plazo }, { label: 'PESAJE', value: lb.pesaje },
28606            { label: 'DESBASTE', value: lb.desbaste }, { label: 'ALIMENTACIÓN', value: lb.pastoreo },
28607            { label: 'SANIDAD', value: lb.sanidad }, { label: 'GARRAPATA', value: lb.garrapata },
28608            { label: 'MÍO MÍO', value: booleanValue(lb.hasMioMio) }, { label: 'APTO CHINA', value: this.rosganTvIsAptoChina(lb) ? 'Sí' : '' },
28609            { label: 'TÉRMINO', value: [true, 1, 'true', '1', 'si', 'sí'].includes(typeof lb.isTermino === 'string' ? lb.isTermino.trim().toLowerCase() : lb.isTermino) ? 'Sí' : '' }, { label: 'MARCA LÍQUIDA', value: booleanValue(lb.invernada?.hasLiquidBrand) },
28610
28611          ]) },
28612          { label: 'VIENTRES', cards: lotCards([
28613            { label: 'SERVICIO', value: lb.servicio }, { label: 'PREÑEZ', value: lb.prenez }, { label: 'PARICIÓN', value: lb.paricion },
28614          ]) },
28615          { label: 'FAENA', cards: lotCards([
28616            { label: 'SUBCATEGORÍA', value: lb.faena_subcategoria }, { label: 'CONDICIÓN', value: lb.faena_condicion },
28617            { label: 'ENTREGA', value: lb.faena_entrega }, { label: 'TERMINACIÓN', value: lb.faena_terminacion },
28618            { label: 'RINDE ESTIMADO', value: lb.faena_rinde },
28619          ]) },
28620        ].filter((group) => group.cards.length) : [];
28621        return {
28622          rdColor: t[1], rdType: t[0].toUpperCase(), rdInitial: rdConsignataria.trim()[0].toUpperCase(),
28623          ...(() => {
28624            const consignatariaRef = rdConsignataria;
28625            const whiteLogo = this.consWhiteLogo(consignatariaRef);
28626            const logo = whiteLogo || this.consLogo(consignatariaRef);
28627            return { rdLogo: logo, rdHasLogo: !!logo, rdNoLogo: !logo, rdLogoBg: whiteLogo ?this.consBrandColor(consignatariaRef) : '#fff', rdLogoBorder: whiteLogo ?'transparent' : 'rgba(255,255,255,.32)' };
28628          })(),
28629          rdTitle: rm.nombre_remate || 'Remate histórico',
28630          rdDate: dd ?dd.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' }) + (rm.hora_remate ?' · ' + String(rm.hora_remate).slice(0, 5) + ' h' : '') : 'Fecha pendiente',
28631          rdLocation: [this.remateListPlace(rm).venue, this.remateListPlace(rm).location].filter(Boolean).join(' · '),
28632          rdConsignataria,
28633          ...window.RosganAuctionShare.prepare({id:remateId,title:rm.nombre_remate||'Remate Rosgan',date:dd?dd.toLocaleDateString('es-AR',{day:'numeric',month:'long',year:'numeric'})+(rm.hora_remate?' · '+String(rm.hora_remate).slice(0,5)+' h':''):'',location:rm.ubicacion_remate||[rm.localidad,rm.provincia].filter(Boolean).join(', '),consignataria:rdConsignataria,heads:this.remateManualShareHeads(rm),path:'/remates/'+encodeURIComponent(remateId)},()=>this.setState({remateShareReady:Date.now()})),
28634          rdCanAdminRemate: isActiveRosgan && this.canManageUsers(s.apiUser),
28635          editCurrentRosganRemate: () => this.openRosganRemateForm(rm),
28636          deleteCurrentRosganRemate: () => this.deleteRosganRemate(rm),
28637          rdRemateDeleting: s.rosganRemateDeleting === remateId,
28638          rdDeleteRemateLabel: s.rosganRemateDeleting === remateId ?'Eliminando…' : 'Eliminar remate',
28639          rdHasStats: raw.length > 0 || totalHeads > 0,
28640          rdStats: [
28641            ...(raw.length ?[{ value: raw.length.toLocaleString('es-AR'), label: 'LOTES' }] : []),
28642            ...(totalHeads ?[{ value: totalHeads.toLocaleString('es-AR'), label: 'CABEZAS' }] : []),
28643          ],
28644          rdHasCategoryHeads: categoryHeads.length > 0,
28645          rdCategoryHeads: categoryHeads.map((item) => {
28646            const active = (selectedFilters.category || []).includes(item.label);
28647            return { label: item.label, value: item.heads.toLocaleString('es-AR'), active, bg: active ?'rgba(56,166,255,.32)' :'rgba(255,255,255,.08)', border: active ?'#80c8ff' :'rgba(255,255,255,.14)', pick: () => toggleCategory(item.label) };
28648          }),
28649          rdCatalogPublished: s.catalogPublication?.key === this.catalogPublicationKey(rm) && s.catalogPublication?.published === true,
28650          rdCompanyCatalogAvailable: !!rdOwnCompanyRef && s.catalogPublication?.key === this.catalogPublicationKey(rm) && s.catalogPublication?.published === true,
28651          rdCompanyCatalogColor: this.consBrandColor(rdOwnCompanyName),
28652          openRdCompanyCatalog: () => {
28653            if(!rdOwnCompanyRef)return;
28654            const dialog=document.createElement('dialog');dialog.className='rd-company-dialog';
28655            dialog.style.setProperty('--company-color',this.consBrandColor(rdOwnCompanyRef));
28656            const make=(tag,cls,text)=>{const node=document.createElement(tag);node.className=cls;if(text)node.textContent=text;return node;};
28657            const header=make('div','rd-company-dialog-header');
28658            const brand=make('div','rd-company-dialog-brand'),logo=this.consWhiteLogo(rdOwnCompanyRef)||this.consLogo(rdOwnCompanyRef);
28659            if(logo){const img=make('img','');img.src=logo;img.alt='';brand.append(img);if(!this.consWhiteLogo(rdOwnCompanyRef))brand.style.background='#fff';}else brand.textContent=rdOwnCompanyName.slice(0,1);
28660            const identity=make('div','rd-company-dialog-identity');identity.append(make('span','rd-company-dialog-eyebrow','CATÁLOGO DE TU CONSIGNATARIA'),make('strong','',rdOwnCompanyName));header.append(brand,identity);
28661            const close=make('button','rd-company-dialog-dismiss','×');close.type='button';close.setAttribute('aria-label','Cerrar');close.onclick=()=>dialog.close();header.append(close);
28662            const body=make('div','rd-company-dialog-body'),heading=make('h2','','¿Qué versión querés descargar?');heading.id='rd-company-dialog-title';dialog.setAttribute('aria-labelledby',heading.id);
28663            body.append(heading,make('p','rd-company-dialog-intro','Los mismos lotes, con la información que necesitás compartir.'));
28664            const actions=make('div','rd-company-dialog-actions');
28665            for(const [variant,text,description,icon] of [['con','Con comitentes','Incluye el nombre del c
28665omitente en cada lote.','users-round'],['sin','Sin comitentes','Muestra los datos del lote sin identificar al comitente.','file-text']]){
28666              const button=make('button','rd-company-dialog-option');button.type='button';
28667              const symbol=make('span','rd-company-dialog-symbol'),img=make('img','');img.src='/assets/icons/lucide/'+icon+'.svg';img.alt='';symbol.append(img);
28668              const download=make('span','rd-company-dialog-download','Descargar PDF'),downloadIcon=make('img','');downloadIcon.src='/assets/icons/lucide/download.svg';downloadIcon.alt='';downloadIcon.width=16;downloadIcon.height=16;download.style.display='flex';download.style.alignItems='center';download.style.gap='6px';download.append(downloadIcon);
28669              const copy=make('span','rd-company-dialog-option-copy');copy.append(make('strong','',text),make('span','',description),download);button.append(symbol,copy);
28670              button.onclick=()=>{const query=new URLSearchParams({remate:String(remateId),consignataria:rdOwnCompanyName,consignatariaId:rdOwnCompanyRef,version:variant});if(!isActiveRosgan)query.set('historico','1');window.location.href='/catalogo.html?'+query;};actions.append(button);
28671            }
28672            body.append(actions,make('div','rd-company-dialog-format','PDF · A4 horizontal · Un lote por página'));
28673            dialog.append(header,body);dialog.addEventListener('close',()=>dialog.remove(),{once:true});document.body.append(dialog);dialog.showModal();;
28674          },
28675          openRdCatalog: () => { window.location.href = '/catalogo.html?remate=' + encodeURIComponent(remateId) + (isActiveRosgan ? '' : '&historico=1'); },
28676          rdFiltersOpen:s.rdLotFiltersOpen===filterId,rdFilterButton:'Filtros'+(filterCount?' · '+filterCount:''),
28677          rdHasActiveFilters: filterCount > 0,
28678          rdActiveFilterCount: String(filterCount),
28679          toggleRdFilters:()=>this.setState({rdLotFiltersOpen:s.rdLotFiltersOpen===filterId?null:filterId}),
28680          clearRdFilters:()=>this.setState({rdLotFilters:{...s.rdLotFilters,[filterId]:{}}}),
28681          rdFilterGroups: filterGroups.map(group => ({ ...group, icon: ({ category: '/assets/icons/lucide/tag.svg', consignor: '/assets/icons/lucide/building-2.svg', breed: '/assets/icons/lucide/beef.svg', term: '/assets/icons/lucide/clock-3.svg', province: '/assets/icons/lucide/map.svg', locality: '/assets/icons/lucide/map-pin.svg', visec: '/assets/icons/visec-mark.svg', china: '/assets/icons/china-flag.svg' })[group.key], options: group.options.map(option => {
28682            const brand = group.key === 'consignor' ?this.lotDestinationLogos({ consignatariaId: option.value, consignatariaNombre: option.value })[0] : null;
28683            return { ...option, label: brand?.name || (group.alwaysVisible ? 'Solo aptos' : option.value), hasLogo: !!brand?.logo, logo: brand?.logo || '', logoBg: brand?.color || '#fff',
28684              bg: option.active ?'#003767' :'#fff', color: option.active ?'#fff' :'#375875',
28685              pick: () => { const latest = this.state.rdLotFilters?.[filterId] || {}; const values = latest[group.key] || [];
28686                this.setState({ rdLotFilters: { ...this.state.rdLotFilters, [filterId]: { ...latest, [group.key]: values.includes(option.value) ?values.filter(v=>v!==option.value) : [...values,option.value] } } });
28687              }
28688            };
28689          }) })),
28690          rdQuery: s.rdQuery, onRdQuery: (e) => this.setState({ rdQuery: e.target.value }),
28691          rdSort: s.rdSort, onRdSort: (e) => this.setState({ rdSort: e.target.value }),
28692          rdCanManageSalesOrder: isActiveRosgan && this.canManageUsers(s.apiUser),
28693          rdCanViewAuditor: isActiveRosgan && !!s.user && (this.canManageUsers(s.apiUser) || this.canManageSellerData(s.apiUser) || this.isRosganProfile(s.apiUser)),
28694          openRdSalesOrder: () => this.openRosganSalesOrder(),
28695          rdAuditorLabel: this.isRosganProfile() ? 'Panel Consignatario' : 'Panel Auditor',
28696          openRdAuditor: () => this.openRosganAuditor(rm),
28697          rdShowOperations: isActiveRosgan,
28698          rdCanManageOperations: isActiveRosgan && this.canOpenRosganOperation(rm, 'commercial'),
28699          rdCanViewStats: isActiveRosgan && this.canOpenRosganOperation(rm, 'stats'),
28700          openRdCommercial: () => this.openRosganOperations(rm, 'commercial'),
28701          openRdStats:()=>this.openCurrentRosganOps('stats'),
28702          openRdPrices: () => this.openRosganOperations(rm, 'prices'),
28703          openRdTv: () => this.openRosganOperations(rm, 'tv'),
28704          openRdTvConfig: () => this.openRosganOperations(rm, 'tvConfig'),
28705          setGridView: () => this.setState({ rdView: 'grid' }), setListView: () => this.setState({ rdView: 'list' }),
28706          gridViewBg: s.rdView === 'grid' ?'#002e5f' : 'transparent', gridViewColor: s.rdView === 'grid' ?'#fff' : '#8593a6',
28707          listViewBg: s.rdView === 'list' ?'#002e5f' : 'transparent', listViewColor: s.rdView === 'list' ?'#fff' : '#8593a6',
28708          rdCount: list.length, rdTotal: raw.length,
28709          rdSourceNote: '',
28710          rdLoading: !!s.remateLotsLoading,
28711          rdLoaded: !s.remateLotsLoading,
28712          rdShowGrid: !s.remateLotsLoading && s.rdView === 'grid' && list.length > 0, rdShowList: !s.remateLotsLoading && s.rdView === 'list' && list.length > 0,
28713          rdEmpty: !s.remateLotsLoading && !!s.remateLots && list.length === 0,
28714          rdEmptyTitle: raw.length === 0 ?'Este remate aún no tiene lotes publicados' : 'No encontramos lotes para esa búsqueda',
28715          rdEmptyText: raw.length === 0 ?'Los lotes se publican cuando el remate se aproxima.' : 'Probá con otro término.',
28716          rdLots: (s.route==='remateDetail'?list:[]).map((l, i) => {
28717            const consignataria = this.consDisplayName(l.nombre_consignatario || rdConsignataria) || 'Rosgan';
28718            const whiteLogo = this.consWhiteLogo(consignataria);
28719            const logo = whiteLogo || this.consLogo(consignataria);
28720            const alertCert = this.remateLotRouteId(l);
28721            const alertKey = this.remateLotAlertKey(remateId, alertCert);
28722            const alert = s.remateLotAlerts?.[alertKey] || null;
28723            const alertBusy = s.remateLotAlertBusy === alertKey;
28724            const alertEnabled = alert?.enabled === true && alert?.
28724status !== 'notified';
28725            const alertSent = alert?.status === 'notified';
28726            const raza = hasLotValue(l.raza) ?String(l.raza).trim() : '';
28727            const localidad = loc(l);
28728            const cantidad = heads(l);
28729            const pesoPromedio = wt(l);
28730            const cert = l.nro_certificado || l.id_certificado || '';
28731            const share=window.RosganAuctionShare.prepare({kind:'lot',brandLogo:logo,brandColor:this.consBrandColor(consignataria),slogan:window.rosganConsignatariaSlogan?.(consignataria)||'',title:l.nombre_categoria||raza||'Lote',cert,breed:raza,heads:cantidad,weight:pesoPromedio,location:localidad,consignataria,auction:rm.nombre_remate||rm.titulo||'Remate Rosgan',details:{maleQuantity:l.cantidad_machos,femaleQuantity:l.cantidad_hembras,minWeight:l.peso_minimo,maxWeight:l.peso_maximo,formType:l.formType||(l.faena?'faena':l.vientres?'vientres':''),isVientres:!!l.vientres,age:l.edad||String(l.descripcion_lote||'').match(/(?:^|[·;\n])\s*Edad\s*:\s*([^·;\n]+)/i)?.[1]||'',dentition:l.denticion,reproductiveStatus:l.estado_reproductivo,service:l.servicio,pregnancy:l.prenez,calving:l.paricion,terms:l.plazo,destination:l.destino,condition:l.faena_condicion,delivery:l.faena_entrega},image:l.thumbnailUrl||l.thumbnail||l.imagen||'',video:l.videoUrl||l.video_uri||'',path:'/remates/'+encodeURIComponent(remateId)+'/'+encodeURIComponent(alertCert)},()=>this.setState({lotShareReady:Date.now()}));
28732
28733            return {
28734              shareLot:event=>{event?.stopPropagation();share.shareCurrentRemate();}, shareReady:share.rdShareReady, shareLabel:share.rdShareReady?'Compartir lote':'Preparando imagen…',
28735              open: () => this.openRemateLot(l), hoverIn, hoverOut,
28736              thumb: l.thumbnailUrl || l.thumbnail || l.imagen || (isActiveRosgan ? '' : 'assets/angus-field.png'),
28737              thumbnailPending: isActiveRosgan && !(l.thumbnailUrl || l.thumbnail || l.imagen),
28738              thumbSize: 'cover',
28739              thumbBg: '#e4edf5',
28740              videoPreview: l.videoUrl || l.video_uri || '',
28741              number: lotNum(l), numberShort: lotNumber(l) ?? '', showNumber: !!lotNum(l),
28742              cert: cert || 's/c', categoria: l.nombre_categoria || raza || 'Lote',
28743              raza, showRaza: !!raza, localidad, cabezas: cantidad || '', showCabezas: cantidad > 0,
28744              peso: pesoPromedio ?pesoPromedio + ' kg' : '', showPeso: pesoPromedio > 0,
28745              listMeta: [cert ?`Cert. ${cert}` : '', raza, localidad].filter(hasLotValue).join(' · '),
28746              facts: factsOf(l),
28747              consignataria, consignatariaLogo: logo, hasConsignatariaLogo: !!logo, noConsignatariaLogo: !logo,
28748              consignatariaLogoBg: whiteLogo ?this.consBrandColor(consignataria) : (logo ?'#fff' : this.consBrandColor(consignataria)),
28749              consignatariaInitial: String(consignataria).trim().charAt(0).toUpperCase() || 'R',
28750              showAlert: isActiveRosgan,
28751              toggleAlert: (event) => this.toggleRemateLotAlert(rm, l, event),
28752              alertBusy,
28753              alertDisabled: alertBusy || alertSent,
28754              alertAriaPressed: alertEnabled ?'true' : 'false',
28755              alertLabel: alertBusy ?'Guardando…' : alertEnabled ?'Aviso activado' : alertSent ?'Aviso enviado' : 'Avisame cuando sale',
28756              alertIcon: 'assets/icons/lucide/' + (alertEnabled || alertSent ?'bell-ring' : 'bell') + '.svg',
28757              alertBg: alertEnabled ?'#e7f2ff' : alertSent ?'#eef2f6' : '#fff',
28758              alertColor: alertEnabled ?'#075da8' : alertSent ?'#718196' : '#003f78',
28759              alertBorder: alertEnabled ?'#75b8ee' : alertSent ?'#d8e0e8' : '#b8cfe2',
28760            };
28761          }),
28762          rdLightbox: false, rdLotPage: !!lb, rdCatalogPage: !lb,
28763          lbHasPreviousLot: !!lbNeighbors.previous, lbHasNextLot: !!lbNeighbors.next,
28764          lbPreviousLot: () => { if (lbNeighbors.previous) this.openRemateLot(lbNeighbors.previous); },
28765          lbNextLot: () => { if (lbNeighbors.next) this.openRemateLot(lbNeighbors.next); },
28766          ...(() => {if(!lb)return {};const share=window.RosganAuctionShare.prepare({kind:'lot',brandLogo:lbConsignatariaLogo,brandColor:lbConsignatariaColor,slogan:window.rosganConsignatariaSlogan?.(lbConsignataria)||'',title:lb.nombre_categoria||lb.raza||'Lote',cert:lb.nro_certificado,breed:lb.raza,heads:heads(lb),weight:wt(lb),location:loc(lb),consignataria:lbConsignataria,auction:rm.nombre_remate||rm.titulo||'Remate Rosgan',details:{maleQuantity:lb.cantidad_machos,femaleQuantity:lb.cantidad_hembras,minWeight:lb.peso_minimo,maxWeight:lb.peso_maximo,formType:lb.formType||(lb.faena?'faena':lb.vientres?'vientres':''),isVientres:!!lb.vientres,age:lbAge,dentition:lb.denticion,reproductiveStatus:lb.estado_reproductivo,service:lb.servicio,pregnancy:lb.prenez,calving:lb.paricion,terms:lb.plazo,destination:lb.destino,condition:lb.faena_condicion,delivery:lb.faena_entrega},image:lb.thumbnailUrl||lb.thumbnail||lb.imagen||'',video:lbPrimaryVideoUrl,path:'/remates/'+encodeURIComponent(remateId)+'/'+encodeURIComponent(lb.nro_certificado)},()=>this.setState({lotShareReady:Date.now()}));return {shareCurrentLot:share.shareCurrentRemate,lbShareReady:share.rdShareReady,lbShareLabel:share.rdShareReady?'Compartir lote':'Preparando imagen…'};})(),
28767          lbThumb: lb ?(lb.thumbnailUrl || lb.thumbnail || lb.imagen || 'assets/angus-field.png') : '',
28768          lbHasNumber: !!lb && !!lotNum(lb),
28769          lbNumber: lb ?lotNum(lb) : '', lbCert: lb?.nro_certificado || 's/c', lbCategoria: lb?.nombre_categoria || lb?.raza || 'Lote', lbRaza: lb?.raza || 'Sin raza',
28770          lbBreedIcons, lbHasBreedIcons: lbBreedIcons.length > 0,
28771          lbFacts: lb ?factsOf(lb) : [],
28772          lbLocation: lb ?loc(lb) : '', lbDetailGroups,
28773          lbShowVisec: !!lb && this.rosganTvIsAptoVisec(lb), lbShowChina: !!lb && this.rosganTvIsAptoChina(lb),
28774          rdLotDetailCols: s.compactNavigation ?'minmax(0,1fr)' : 'minmax(0,1.55fr) minmax(280px,.72fr)', rdLotAsidePosition: s.compactNavigation ?'static' : 'sticky',
28775          lbPrimaryVideoUrl, lbNoPrimaryVideo: !lbPrimaryVideoUrl,
28776          lbVideoPlay: (event) => this.syncCabanaVideoState(event?.currentTarget, true),
28777          lbVideoPause: (event) => this.syncCabanaVideoState(event?.currentTarget, true),
28778          lbVideoMeta: (event) => this.syncCabanaVideoState(event?.currentTarget, true),
28779          lbVideoTimeUpdate: (event) => this.syncCabanaVideoState(event?.currentTarget, false),
28780          lbVideoVolumeChange: (event) => { const video=event?.currentTarget; if(video){video.muted=true;video.defaultMuted=true;} this.syncCabanaVideoState(video, true); },
28781          lbVideoEnded: (event) => { const video = event?.currentTarget; if (video) { video.currentTime = 0; video.play?.().catch(() => {}); this.syncCabanaVideoState(video, true); } },
28782          lbTogglePlay: (event) => this.toggleCabanaVideoPlayback(event), lbSeekVideo: (event) => this.seekCabanaVideo(event),
28783          lbToggleMute: (event) => this.toggleCabanaVideoMute(event), lbSetVolume: (event) => this.setCabanaVideoVolume(event),
28784          lbFullscreen: (event) => this.openCabanaVideoFullscreen(event),
28785          lbPlayIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoPlaying ?'pause' : 'play') + '.svg',
28786          lbPlayLabel: s.cabanaLotVideoPlaying ?'Pausar video' : 'Reproducir video',
28787          lbVolumeIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoMuted ?'volume-x' : 'volume-2') + '.svg',
28788          lbMuteLabel: s.cabanaLotVideoMuted ?'Activar sonido' : 'Silenciar video',
28789          lbVideoProgress: Math.round(Number(s.cabanaLotVideoProgress || 0) * 10) / 10,
28790          lbVideoVolume: Math.round(Number(s.cabanaLotVideoVolume || 0) * 100) / 100,
28791          lbVideoTime: this.formatCabanaVideoTime((Number(s.cabanaLotVideoProgress || 0) / 100) * Number(s.cabanaLotVideoDuration || 0)) + ' / ' + this.formatCabanaVideoTime(s.cabanaLotVideoDuration),
28792          lbHasObservations: !!lbObservations, lbObservations,
28793          lbConsignataria, lbConsignatariaLogo, lbHasConsignatariaLogo: !!lbConsignatariaLogo, lbNoConsignatariaLogo: !lbConsignatariaLogo,
28794          lbConsignatariaInitial: lbConsignataria.trim().charAt(0).toUpperCase(), lbConsignatariaColor, lbConsignatariaShadow: lbConsignatariaColor + '35',
28795          lbHasRepresentative: !!lbRepresentative,
28796          lbRepresentativeName: lbRepresentative?.name || '', lbRepresentativePhone: lbRepresentative?.phone || '',
28797          lbRepresentativeHasPhone: !!lbRepresentative?.hasPhone, lbRepresentativeHasWhatsapp: !!lbRepresentative?.hasWhatsapp,
28798          lbRepresentativeHasContact: !!lbRepresentative && !lbRepresentative.noContact, lbRepresentativeNoContact: !!lbRepresentative?.noContact,
28799          lbRepresentativePhoneHref: lbRepresentative?.phoneHref || '', lbRepresentativeWhatsappHref: lbRepresentative?.whatsappHref || '',
28800          lbRepresentativePhotoClass: lbRepresentativeAvatar.photo ? 'dc-representative-photo' : '',
28801          lbRepresentativeAvatar: lbRepresentativeAvatar.avatar, lbRepresentativeHasAvatar: lbRepresentativeAvatar.hasAvatar,
28802          lbRepresentativeAvatarBg: lbRepresentativeAvatar.bg, lbRepresentativeAvatarSize: lbRepresentativeAvatar.photo ? '100%' : '30px', lbRepresentativeAvatarFit: lbRepresentativeAvatar.fit,
28803          lbRepresentativeAvatarError: () => { if (lbRepresentativeAvatar.photo && lbRepresentative?.uid) this.setState({ rosganRepresentativePhotoFailed: { ...this.state.rosganRepresentativePhotoFailed, [lbRepresentative.uid]: true } }); },
28804          lbRosganVideoManager: isActiveRosgan && !!lb,
28805          lbCanManageVideos: canManageVideos,
28806          lbCanEditRosganLot: isActiveRosgan && this.canEditRosganLot(lb), editRosganLot: () => this.openRosganLotEditor(lb),
28807          uploadRosganLotVideos: (event) => { const files = event.target.files; this.uploadRosganLotVideos(files); event.target.value = ''; },
28808          prepareFieldLot: () => this.prepareFieldLot(),
28809          lbVideoTitle: (videoState.videos || []).length ?(videoState.videos || []).length + ((videoState.videos || []).length === 1 ?' video cargado' : ' videos cargados') : 'Material audiovisual',
28810          lbVideoLoading: !!videoState.loading,
28811          lbVideoHasError: !!videoState.error,
28812          lbVideoError: videoState.error || '',
28813          lbVideoUploadBusy: !!videoState.uploading,
28814          lbVideoHasUploads: !!(videoState.uploads || []).length,
28815          lbVideoUploads: (videoState.uploads || []).map((upload) => ({
28816            name: upload.name || 'Video', progress: Math.max(0, Math.min(100, Number(upload.progress) || 0)), size: this.formatFileSize(upload.sizeBytes),
28817            status: upload.status || 'En espera', color: upload.status === 'Error' ?'#b42332' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8',
28818            bar: upload.status === 'Error' ?'#d94a58' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8',
28819            error: upload.error || '', hasError: !!upload.error, canRetry: !!upload.canRetry, retryLabel: upload.retryLabel, retry: () => this.retryRosganVideoUpload(upload.id),
28820          })),
28821          lbVideoHasItems: !videoState.loading && !!(videoState.videos || []).length,
28822          lbVideoEmpty: !videoState.loading && !(videoState.videos || []).length && !(videoState.uploads || []).some((upload) => upload.status !== 'Error'),
28823          lbVideos: (videoState.videos || []).map((video, index) => {
28824            const id = String(video?.id || video?.videoId || video?.videoID || '');
28825            const url = video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl || '';
28826            const name = video?.fileName || video?.name || video?.title || ('Video ' + (index + 1));
28827            return {
28828              id, url, hasUrl: !!url, noUrl: !url, name, size: this.formatFileSize(video?.sizeBytes || video?.fileSize || video?.size),
28829               canDelete: canManageVideos && !!id && String(video?.videoType || video?.type || '').toLowerCase() === 'raw', deleting: s.rosganLotVideoDeleting === videoDeletingPrefix + id,
28830              remove: () => this.deleteRosganLotVideo(video),
28831            };
28832          }),
28833          closeLightbox: () => this.closeRemateLot(), stopProp: (e) => e.stopPropagation(),
28834          rgOrderCommandApply:()=>this.applyRosganOrderCommand(),
28835          rgOrderCommandEnter:event=>{if(event.key==='Enter'&&!event.isComposing&&!event.repeat){event.preventDefault();this.applyRosganOrderCommand();}},
28836          rgOrderCommandMessage:s.rosganOrderCommandMessage||'',
28837          rgCanToggleLotLoading: this.canManageUsers(s.apiUser),
28838          rgLotLoadingEnabled: rm.cargaDeLotes !== false,
28839          rgLotLoadingBusy: !!s.remateLotLoadingBusy,
28840          rgLotLoadingLabel: s.remateLotLoadingBusy ? 'Guardando…' : rm.cargaDeLotes !== false ? 'Carga de lotes abierta' : 'Carga de lotes cerrada',
28841          toggleRgLotLoading: () => this.setRemateLotLoading(rm, rm.cargaDeLotes === false).catch(e => this.showToast(e.message)),
28842          rgSalesOrderTitle: rm.nombre_remate || 'Remate Rosgan',
28843          rgSalesOrderCount: rosganOrderLots.length,
28844          rgSalesOrderUnratedCount: rosganOrderLots.filter(lot => !(Number(s.rosganSalesOrderRatings?.[this.remateLotRouteId(lot)]) > 0)).length,
28845          rgSalesOrderVisibleCount: rosganOrderRows.length,
28846          rgSalesOrderRows: rosganOrderRows,
28847          rgSalesOrderLoading: !!s.remateLotsLoading,
28848          rgSalesOrderEmpty: !s.remateLotsLoading && rosganOrderLots.length === 0,
28849          rgSalesOrderNoResults: !s.remateLotsLoading && rosganOrderLots.length > 0 && rosganOrderRows.length === 0,
28850          rgSalesOrderRatingFilter: orderRatingFilter,
28851          setRgSalesOrderRatingFilter: event => this.setState({ rosganSalesOrderRatingFilter: event.target.value, rosganSalesOrderRatingSnapshot: { ...s.rosganSalesOrderRatings } }),
28852          rgSalesOrderQuery: s.rosganSalesOrderQuery || '',
28853          setRgSalesOrderQuery: (event) => this.setState({ rosganSalesOrderQuery: event.target.value }),
28854          rgSalesOrderStatusFilter: orderStatusFilter,
28855          setRgSalesOrderStatusFilter: (event) => this.setState({ rosganSalesOrderStatusFilter: event.target.value }),
28856          rgSalesOrderCategoryFilter: orderCategoryFilter,
28857          setRgSalesOrderCategoryFilter: (event) => this.setState({ rosganSalesOrderCategoryFilter: event.target.value }),
28858          rgSalesOrderConsignatariaFilter: orderConsignatariaFilter,
28859          setRgSalesOrderConsignatariaFilter: (event) => this.setState({ rosganSalesOrderConsignatariaFilter: event.target.value }),
28860          rgSalesOrderStatusOptions: [{ value: '', label: 'Todos los estados' }, ...Array.from(new Set(rosganOrderLots.map((lot) => this.rosganLotWorkflowStatus(lot)))).sort().map((status) => ({ value: status, label: orderStatusLabels[status] || status }))],
28861          rgSalesOrderCategoryOptions: [{ value: '', label: 'Todas las categorías' }, ...Array.from(new Set(rosganOrderLots.map(orderCategoryOf))).sort((a, b) => a.localeCompare(b, 'es')).map((value) => ({ value, label: value }))],
28862          rgSalesOrderConsignatariaOptions: [{ value: '', label: 'Todas las consignatarias' }, ...Array.from(new Set(rosganOrderLots.map(orderConsignatariaOf))).sort((a, b) => a.localeCompare(b, 'es')).map((value) => ({ value, label: value }))],
28863          rgSalesOrderHasError: !!s.rosganSalesOrderError,
28864          rgSalesOrderError: s.rosganSalesOrderError || '',
28865          rgSalesOrderSaved: !!s.rosganSalesOrderSaved,
28866          rgSalesOrderSaveLabel: s.rosganSalesOrderSaving ?'Guardando…' : 'Guardar orden',
28867          rgCatalogPublished: s.catalogPublication?.key === this.catalogPublicationKey() && s.catalogPublication?.published === true,
28868          rgCatalogBusy: !s.catalogPublication?.loaded || !!s.catalogPublication?.busy,
28869          rgCatalogOpacity: !s.catalogPublication?.loaded || s.catalogPublication?.busy ? '.55' : '1',
28870          rgCatalogSwitchColor: s.catalogPublication?.published ? '#008D46' : '#aab9c9',
28871          rgCatalogSwitchOffset: s.catalogPublication?.published ? 14 : 0,
28872          toggleRgCatalog: () => this.toggleCatalogPublication(),
28873          rgSalesOrderSaveOpacity: s.rosganSalesOrderSaving ?'.62' : '1',
28874          rgPreorderDisabled: !!s.rosganPreorderExporting || !!s.remateLotsLoading || !rosganOrderLots.length,
28875          rgPreorderLabel: s.rosganPreorderExporting ? 'Generando preorden…' : 'Obtener preorden',
28876          exportRgPreorderExcel: () => this.exportRosganPreorderExcel(),
28877          rgSalesOrderImportLabel: s.rosganSalesOrderImporting ?'Leyendo Excel…' : 'Cargar Excel',
28878          rgSalesOrderImportClass: s.rosganSalesOrderImporting ?'is-busy' : '',
28879          importRgSalesOrderExcel: (event) => this.importRosganSalesOrderExcel(event),
28880          rgSalesOrderHasImport: orderSchedule.startSeconds != null && orderSchedule.entries.length > 0,
28881          rgSalesOrderImportFile: orderSchedule.fileName || 'Orden importado desde Excel',
28882          rgSalesOrderImportStart: window.RosganSalesOrderImport.clock(orderSchedule.startSeconds),
28883          rgSalesOrderImportEnd: window.RosganSalesOrderImport.clock(orderSchedule.endSeconds),
28884          rgSalesOrderImportGroups: new Set(orderSchedule.entries.map((entry) =>
28884 entry.alias)).size,
28885          closeRgSalesOrder: () => this.closeRosganSalesOrder(),
28886          uploadRgCatalogCover: async event => {
28887            const file = event.target.files?.[0]; event.target.value = '';
28888            if (!file || !this.canManageUsers(this.state.apiUser)) return;
28889            try { await window.RosganCatalogCover.preview(file,{remateId:this.remateRouteId(this.state.currentRemate),apiFetch:(url,options)=>this.apiFetch(url,options),onSaved:()=>this.setState({rosganOrderCommandMessage:'Tapa del catálogo guardada.'})}); }
28890            catch(error) { this.setState({rosganSalesOrderError:error.message || 'No se pudo cargar la tapa.'}); }
28891          },
28892          configureRgCatalogNews: () => window.RosganCatalogCover.news({remateId:this.remateRouteId(this.state.currentRemate),apiFetch:(url,options)=>this.apiFetch(url,options)}),
28893          uploadRgCatalogBackCover: async event => {
28894            const file = event.target.files?.[0]; event.target.value = '';
28895            if (!file || !this.canManageUsers(this.state.apiUser)) return;
28896            try { await window.RosganCatalogCover.preview(file,{backCover:true,remateId:this.remateRouteId(this.state.currentRemate),apiFetch:(url,options)=>this.apiFetch(url,options),onSaved:()=>this.setState({rosganOrderCommandMessage:'Contratapa del catálogo guardada.'})}); }
28897            catch(error) { this.setState({rosganSalesOrderError:error.message || 'No se pudo cargar la contratapa.'}); }
28898          },
28899          saveRgSalesOrder: () => this.saveRosganSalesOrder(),
28900        };
28901      })(),
28902      ...(() => {
28903        const remate = s.currentRemate || {};
28904        const lots = (Array.isArray(s.remateLots) ?s.remateLots : []).slice().sort((a, b) => this.compareRosganSalesOrder(a,b));
28905        const state = this.normalizeRosganOpsState({ commercial: s.rosganOpsDrafts, tv: s.rosganOpsConfig }, remate);
28906        const money = (value) => {
28907          const number = Number(String(value ?? '').replace(/[^0-9,.-]/g, '').replace(',', '.')) || 0;
28908          return number ?'$ ' + Math.round(number).toLocaleString('es-AR') : '—';
28909        };
28910        const numberText = (value) => {
28911          const number = Number(value) || 0;
28912          return number ?number.toLocaleString('es-AR') : '—';
28913        };
28914        const priceOf = (draft = {}) => Number(draft.priceMode === 'kilo' ?draft.precioKilo : draft.precioCabeza) || 0;
28915        const rowFor = (lot) => {
28916          const cert = this.remateLotRouteId(lot);
28917          const draft = {...state.commercial[cert],...this._rosganPriceEdits?.get(this.remateRouteId(remate)+'/'+cert)?.patch};
28918          const status = String(draft.status || 'pendiente').toLowerCase();
28919          const consignataria = this.consDisplayName(lot.nombre_consignatario || remate.nombre_cuenta || remate.nombre_consignatario) || 'Rosgan';
28920          const temporaryTvConsignataria = this.rosganTvTemporaryConsignataria(remate, lot);
28921          const priceMode = window.RosganPriceInput.mode(lot,draft);
28922          const quantity = Number(lot.cantidad) || 0;
28923          const weight = Number(lot.peso_promedio) || 0;
28924          const locality = [lot.nombre_localidad, lot.nombre_provincia].filter(Boolean).join(', ');
28925          const plazo = lot.plazo || '';
28926          return {
28927            cert,
28928            saleOrder: lot.saleOrder ?? lot.lotNumber ?? lot.orden_venta ?? lot.orden_final,
28929            lotNumber: lot.lotNumber ?? lot.orden_final ?? lot.orden_venta,
28930            order: String(lot.saleOrder ?? lot.orden_final ?? lot.orden_venta ?? ''),
28931            lotLabel: lot.saleOrder || lot.orden_final || lot.orden_venta ?'Lote ' + String(lot.saleOrder || lot.orden_final || lot.orden_venta) : '',
28932            category: lot.nombre_categoria || 'Sin categoría',
28933            formType: lot.formType || lot.tipo || '',
28934            condition: lot.faena_condicion || '',
28935            subcategory: lot.faena_subcategoria || '',
28936            breed: lot.raza || '',
28937            buyerValue: draft.divisions?.length?'Por división':draft.comprador || '',
28938            setBuyer: event => this.editRosganCommercialBuyer(lot,event),
28939            hasDivisions:!!draft.divisions?.length,
28940            divisionLabel:draft.divisions?.length?draft.divisions.length+' partes · '+draft.divisions.reduce((sum,p)=>sum+(Number(p.price)>0?Number(p.quantity):0),0)+' / '+quantity+' vendidas':'Dividir venta',
28941            openDivisions:()=>this.openRosganLotDivisions(lot),
28942            consignataria,
28943            tvConsignataria: temporaryTvConsignataria?.name || '',
28944            tvConsignatariaLogo: temporaryTvConsignataria?.logo || '',
28945            tvConsignatariaColor: temporaryTvConsignataria?.color || '',
28946            quantity,
28947            weight,
28948            plazo,
28949            locality,
28950            dataSummary: [quantity ?quantity + ' cab.' : '', weight ?weight + ' kg' : ''].filter(
28950Boolean).join(' · ') || 'Sin datos de cantidad y peso',
28951            detailSummary: [locality, plazo].filter(Boolean).join(' · '),
28952            status,
28953            statusLabel: status === 'vendido' ?'Vendido' : status === 'levantado' ?'Levantado' : status === 'no-vendido' ?'No vendido' : draft.divisions?.some(p=>Number(p.price)>0)?'Venta parcial':'Pendiente',
28954            statusClass: 'rosgan-ops-status is-' + status,
28955            priceMode,
28956            priceValue: draft.divisions?.length?'Ver divisiones':window.RosganPriceInput.format(priceMode === 'kilo' ?(draft.precioKilo ?? '') : (draft.precioCabeza ?? '')),
28957            priceDisplay: money(priceOf(draft)),
28958            modeLabel: priceMode === 'kilo' ?'$ / kg' : '$ / cabeza',
28959            setPending: () => this.updateRosganCommercial(lot, { status: 'pendiente' }),
28960            setSold: () => this.updateRosganCommercial(lot, { status: 'vendido' }),
28961            setUnsold: () => this.updateRosganCommercial(lot, { status: 'no-vendido' }),
28962            setLifted: () => this.updateRosganCommercial(lot, { status: 'levantado' }),
28963            setKilo: () => this.updateRosganCommercial(lot, { priceMode: 'kilo' }),
28964            setHead: () => this.updateRosganCommercial(lot, { priceMode: 'cabeza' }),
28965            setPrice: (event) => this.editRosganCommercialPrice(lot,priceMode,event),
28966            commitPrice:()=>this.flushRosganCommercialPrice(this.remateRouteId(remate)+'/'+cert),
28967            commitPriceEnter:event=>this.commitRosganCommercialPrice(lot,priceMode,event),
28968            kiloClass: priceMode === 'kilo' ?'is-active' : '',
28969            headClass: priceMode === 'cabeza' ?'is-active' : '',
28970          };
28971        };
28972        const rows = lots.map(rowFor);
28973        const airtime=window.RosganAirtime.summarize(rows,s.rosganAirtime?.remateId===String(this.remateRouteId(remate))?s.rosganAirtime:null,Date.now(),remate.closeRemate===true);
28974        const progress=window.RosganCommercialProgress.progress(rows);
28975        const commercialFilter = s.rosganCommercialFilter || 'todos';
28976        const commercialQuery = this.consKey(s.rosganCommercialQuery || '');
28977        const commercialRows = rows.filter(row => (commercialFilter === 'todos' || row.status === commercialFilter) && this.consKey([row.order,row.cert,row.category,row.breed,row.consignataria,row.locality].join(' ')).includes(commercialQuery));
28978        const soldRows = rows.flatMap(row=>window.RosganSaleDivisions.sales({cantidad:row.quantity},state.commercial[row.cert]||{},row.priceMode).map(part=>({...row,quantity:part.quantity,salePrice:part.price})));
28979        const priceCategoryOrder = ['Terneros','Novillitos','Novillos','Terneros/as','Terneras','Vaquillonas','Vacas de invernada','Terneros Holando','Novillos Holando','Vaq. c/gtía. de preñez','Vacas c/gtía. de preñez','Vacas con cría al pie'];
28980        const priceCategoryFor = row => String(row.formType).toLowerCase() === 'faena' ? [String(row.category).replace(/\s*\([^)]*\)$/, ''),row.subcategory].filter(Boolean).join(' ') : this.rosganPriceCategory(row.category,row.breed);
28981        const localPriceGroups = window.RosganPriceBoard.summarize(soldRows,priceCategoryFor)
28982          .map(group=>({...group,max:money(group.maxValue),average:group.avgValue==null?'—':money(group.avgValue)}))
28983          .sort((a,b)=>priceCategoryOrder.indexOf(a.category)-priceCategoryOrder.indexOf(b.category)||a.unit.localeCompare(b.unit));
28984        const officialPriceState=s.rosganOfficialPrices?.[this.remateRouteId(remate)]||{};
28985        const priceGroups=window.RosganPriceBoard.merge(officialPriceState.groups?.length?officialPriceState.groups:localPriceGroups)
28986          .map(group=>({...group,max:money(group.maxValue),average:group.avgValue==null?'—':money(group.avgValue)}));
28987        const priceStoryGroups = priceGroups;
28988        const priceBrands = [...new Set(rows.map(row=>row.consignataria).filter(Boolean))].map(name=>({name,logo:this.consWhiteLogo(name)||'',color:this.consBrandColor(name)}));
28989        const priceBoardModel = {title:remate.nombre_remate||remate.titulo||'Remate Rosgan',
28990          date:this.remateDate(remate)?.toLocaleDateString('es-AR',{weekday:'long',day:'numeric',month:'long',year:'numeric',timeZone:'America/Argentina/Buenos_Aires'})||'',
28991          location:remate.ubicacion_remate||remate.lugar||'', heads:rows.reduce((n,row)=>n+row.quantity,0), brands:priceBrands,
28992          logo:'/assets/rosgan-logo-blue.png',favicon:'/favicon.png',background:'/assets/lacassina-story-background.png',
28993          mode:s.rosganPriceBoardMode||'category',groups:priceGroups,tables:window.RosganPriceBoard.weightTables(soldRows,priceCategoryFor)};
28994        const selectedCert = this.rosganTvSelectedCert(state.tv, lots);
28995        const tvSlots = this.rosganTvSlots(state.tv, lots);
28996        const onAirCert = s.route === 'rosganTvLower' ?this._rosganTvDecks?.onAirCert : '';
28997        const selectedLot = lots.find((lot) => this.remateLotRouteId(lot) === (onAirCert || selectedCert)) || lots[0] || null;
28998        const selected = s.route === 'rosganTvLower' && selectedLot ?this.rosganTvPreviewLot(selectedLot) : selectedLot;
28999        const extraFields = this.rosganTvExtraFields(selected);
29000        const extraIndex = (s.rosganTvFieldStep || 0) % Math.max(1, extraFields.length);
29001        const tvObservations = String(selected?.observaciones_certificado || selected?.observaciones || selected?.observations || '').replace(/\s+/g, ' ').trim();
29002        const selectedRow = selected ?rowFor(selected) : null;
29003        const temporarySelectedConsignataria = this.rosganTvTemporaryConsignataria(remate, selected);
29004        const selectedConsignataria = temporarySelectedConsignataria?.name || selectedRow?.consignataria || this.consDisplayName(remate.nombre_cuenta || remate.nombre_consignatario) || 'Rosgan';
29005        const consContactKey = this.consKey(selectedConsignataria);
29006        const consRecord = (s.consignatarias || []).find((item) => this.consKey(item.name || item.nombre) === consContactKey);
29007        const rawConsContacts = consRecord?.telefonos || consRecord?.phones || consRecord?.telefono || consRecord?.phone || '';
29008        const consContacts = state.tv.contactsByConsignataria?.[consContactKey] ?? (Array.isArray(rawConsContacts) ?rawConsContacts.map((v) => typeof v === 'string' ?v : v.phone || v.telefono || '').filter(
29008Boolean).join(' | ') : String(rawConsContacts));
29009        const editorEntry = this._rosganTvEdits?.[this.remateRouteId(remate) + '/' + selectedCert];
29010        const consWhiteLogo = temporarySelectedConsignataria?.logo || this.consWhiteLogo(selectedConsignataria);
29011        const consLogo = consWhiteLogo || this.consLogo(selectedConsignataria);
29012        const videoUrl = selected?.videoUrl || selected?.video_uri || '';
29013        const poster = selected?.thumbnailUrl || selected?.thumbnail || selected?.imagen || 'assets/angus-field.png';
29014        const conditions = [selected?.garrapata ?('Garrapata: ' + selected.garrapata) : '', selected?.plazo ?('Plazo: ' + selected.plazo) : ''].filter(Boolean).join('  |  ');
29015        return {
29016          rosCanCloseRemate: this.canManageUsers(s.apiUser) && !!s.currentRemate?.__rosganActive, rosRemateClosingBusy: !!s.remateClosingBusy, rosRemateClosedLabel: s.currentRemate?.closeRemate ?'Reabrir remate':'Cerrar remate', rosToggleRemateClosed: () => this.setRosganRemateClosed(s.currentRemate, !s.currentRemate?.closeRemate),
29017          rosOpsTitle: remate.nombre_remate || remate.titulo || 'Remate Rosgan',
29018          rosOpsLoading: !!s.rosganOpsLoading || !!s.remateLotsLoading,
29019          rosOpsRows: rows,
29020          rosAirtimeResetAll:()=>{if(window.confirm('¿Reiniciar todos los tiempos de este remate? Los lotes al aire seguirán contando desde cero.'))this.updateRosganAirtime({action:'reset-all'});},
29021          rosProgressSegments:progress,
29022          rosProgressTotal:rows.length?rows.length+' lotes · 100%':'Sin lotes',
29023          rosProgressDescription:progress.map(item=>item.label+': '+item.percent+' ('+item.count+' lotes)').join(', '),
29024          rosAirtimeStatus:airtime.status,
29025          rosAirtimeGroups:airtime.groups.map(group=>{const logo=this.consWhiteLogo(group.name)||this.consLogo(group.name);return {...group,logo,hasLogo:!!logo,noLogo:!logo,initial:group.name.charAt(0),color:this.consBrandColor(group.name)};}),
29026          rosAirtimeMetrics:[{label:'Tiempo acumulado de lotes',value:airtime.total},{label:'Promedio por lote',value:airtime.average},{label:'Lotes cronometrados',value:airtime.count},{label:airtime.finished?'Remate finalizado':'Fin estimado · '+airtime.pending+' pendientes',value:airtime.eta}],
29027          rosExportAirtime:async()=>{
29028            if(this._rosganAirtimeExportBusy)return;
29029            this._rosganAirtimeExportBusy=true;
29030            try{
29031              const id=String(this.remateRouteId(remate));
29032              const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/airtime');
29033              if(!result?.data?.totals)throw new Error('No se pudieron obtener los tiempos registrados.');
29034              const snapshot={...result.data,receivedAt:Date.now()};
29035              const capturedAt=Date.now();
29036              const meta={id,title:remate.nombre_remate||remate.titulo,closed:remate.closeRemate===true,date:this.remateDate(remate)?.toLocaleDateString('es-AR'),colors:Object.fromEntries([...new Set(rows.map(row=>row.consignataria))].map(name=>[name,this.consBrandColor(name)]))};
29037              const buffer=await window.RosganAirtime.excel(rows,snapshot,meta,await this.ensureCabanaExcelJs(),capturedAt);
29038              this.downloadCabanaBlob(new Blob([buffer],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}),'tiempos-remate-'+id+'.xlsx');
29039              this.showToast('Informe ejecutivo de tiempos descargado.');
29040            }catch(error){this.showToast(error.message||'No se pudieron exportar los tiempos.');}
29041            finally{this._rosganAirtimeExportBusy=false;}
29042          },
29043          rosCommercialRows: s.route === 'rosganCommercial' ?commercialRows.map(row => {
29044            const logo=this.consWhiteLogo(row.consignataria)||this.consLogo(row.consignataria);
29045            return {...row,
29046              interestLabel:s.rosganLotInterest?.remateId===String(this.remateRouteId(remate))?((s.rosganLotInterest.counts[row.cert]||0)+' '+((s.rosganLotInterest.counts[row.cert]||0)===1?'interesado':'interesados')):'Interesados: —',
29047              editingTime:s.rosganTimeEdit?.id===this.remateRouteId(remate)&&s.rosganTimeEdit?.cert===row.cert,
29048              timeDraft:s.rosganTimeEdit?.value||'',
29049              editTime:()=>this.setState({rosganTimeEdit:{id:this.remateRouteId(remate),cert:row.cert,value:window.RosganAirtime.duration(airtime.totals[row.cert])}}),
29050              changeTime:event=>this.setState({rosganTimeEdit:{...this.state.rosganTimeEdit,value:event.target.value}}),
29051              saveTime:()=>this.saveRosganManualTime(row.cert),cancelTime:()=>this.setState({rosganTimeEdit:null}),
29052              resetTime:()=>this.updateRosganAirtime({action:'set',cert:row.cert,milliseconds:0}),
29053              airtimeLabel:window.RosganAirtime.duration(airtime.totals[row.cert]),airtimeState:airtime.running.includes(row.cert)?'● Al aire':'Tiempo al aire',consLogo:logo,hasConsLogo:!!logo,noConsLogo:!logo,consInitial:row.
29053consignataria.charAt(0),consColor:this.consBrandColor(row.consignataria),
29054              commercialClass:row.status==='vendido'?'is-sold':row.status==='levantado'?'is-lifted':'',
29055              pendingClass:row.status==='pendiente'?'is-pendiente':'',soldClass:row.status==='vendido'?'is-vendido':'',liftedClass:row.status==='levantado'?'is-levantado':'',
29056              unsoldClass:row.status==='no-vendido'?'is-no-vendido':'',isUnsold:row.status==='no-vendido',
29057              isPending:row.status==='pendiente',isSold:row.status==='vendido',isLifted:row.status==='levantado',priceLabel:'Precio del lote '+row.order+' · '+row.modeLabel};
29058          }) :[],
29059          rosCommercialResultCount: commercialRows.length,
29060          rosCommercialEmpty: !s.remateLotsLoading && !s.rosganOpsLoading && commercialRows.length === 0,
29061          rosCommercialQuery: s.rosganCommercialQuery || '',
29062          rosCommercialSearch: event => this.setState({rosganCommercialQuery:event.target.value}),
29063          rosCommercialFilters: [['todos','Todos'],['pendiente','Pendientes'],['vendido','Vendidos'],['no-vendido','No vendidos'],['levantado','Levantados']].map(([key,label]) => ({label,count:key==='todos'?rows.length:rows.filter(row=>row.status===key).length,active:commercialFilter===key,className:'commercial-filter'+(commercialFilter===key?' is-active':''),select:()=>this.setState({rosganCommercialFilter:key})})),
29064          rosCommercialMetrics: [
29065            {label:'Lotes',value:rows.length,icon:'list-ordered',bg:'#e8f2fc'},
29066            {label:'Cabezas',value:numberText(lots.reduce((sum,lot)=>sum+(Number(lot.cantidad)||0),0)),icon:'layers-2',bg:'#edf2fa'},
29067            {label:'Vendidos',value:rows.filter(row=>row.status==='vendido').length,icon:'circle-check-big',bg:'#e7f5ee'},
29068            {label:'Pendientes / levantados',value:rows.filter(row=>row.status==='pendiente').length+' / '+rows.filter(row=>row.status==='levantado').length,icon:'clock-3',bg:'#fff6df'},
29069          ].map(metric=>({...metric,icon:'assets/icons/lucide/'+metric.icon+'.svg'})),
29070          rosOpsLotCount: rows.length,
29071          rosOpsHasRows: rows.length > 0,
29072          rosOpsEmpty: !s.remateLotsLoading && rows.length === 0,
29073          rosOpsSoldCount: rows.filter(row=>row.status==='vendido').length,
29074          rosOpsPendingCount: rows.filter((row) => row.status === 'pendiente').length,
29075          rosOpsLiftedCount: rows.filter((row) => row.status === 'levantado').length,
29076          rosOpsTotalHeads: numberText(lots.reduce((sum, lot) => sum + (Number(lot.cantidad) || 0), 0)),
29077          rosOpsPriceGroups: priceGroups,
29078          rosPriceSource:s.rosganPriceBoardMode==='weight'?'Rangos calculados desde ventas con peso · mesa comercial':officialPriceState.groups?.length?'Resultados oficiales · RosganNet'+(officialPriceState.rosganNetRemateId?' #'+officialPriceState.rosganNetRemateId:''):officialPriceState.loading?'Consultando resultados oficiales…':'Cálculo provisorio · mesa comercial',
29079          rosPriceDescription:(remate.nombre_remate||remate.titulo||'Remate Rosgan')+(officialPriceState.groups?.length?'. Máximos y promedios oficiales publicados por RosganNet.':'. Resultados provisorios calculados desde la mesa comercial.'),
29080          rosPriceConsCount:new Set(rows.map(row=>row.consignataria)).size,
29081          rosPriceExportBusy:!!s.rosganPriceStoryBusy,
29082          rosPriceExportLabel:s.rosganPriceStoryBusy?'Generando Story…':'Descargar Story 9:16',
29083          rosPriceExport:()=>this.downloadRosganPriceStory(priceStoryGroups,priceBoardModel),
29084          rosPriceExportMessage:s.rosganPriceExportMessage||'',
29085          rosPriceBoardData:JSON.stringify(priceBoardModel),
29086          rosPriceCategoryView:()=>this.setState({rosganPriceBoardMode:'category'}),
29087          rosPriceWeightView:()=>this.setState({rosganPriceBoardMode:'weight'}),
29088          rosPriceCategorySelected:priceBoardModel.mode==='category',
29089          rosPriceWeightSelected:priceBoardModel.mode==='weight',
29090          rosOpsHasPrices: priceGroups.length > 0,
29091          rosOpsNoPrices: !s.remateLotsLoading && priceGroups.length === 0,
29092          rosOpsBack: () => this.backToRosganCatalog(),
29093          rosOpsCanAdmin: this.canOpenRosganOperation(s.currentRemate, 'commercial'),
29094          rosOpsOpenCommercial: () => this.openCurrentRosganOps('commercial'),
29095          rosOpsOpenPrices: () => this.openCurrentRosganOps('prices'),
29096          rosBillingExcel:()=>this.downloadRosganBilling('xlsx'),
29097          rosBillingPdf:()=>this.downloadRosganBilling('pdf'),
29098          rosOpsOpenTv: () => this.openCurrentRosganOps('tv'),
29099          rosOpsOpenTvConfig: () => this.openCurrentRosganOps('tvConfig'),
29100          rosTvOptions: lots.filter(lot => String(state.commercial[this.remateLotRouteId(lot)]?.status || '').toLowerCase() !== 'levantado').map((lot) => ({ value: this.remateLotRouteId(lot), label: 'Lote ' + String(lot.orden_final || lot.orden_venta || '—') + ' · ' + (lot.nombre_categoria || 'Sin categoría') })),
29101          rosTvSelectedCert: selectedCert,
29102          rosTvMulti: tvSlots.length > 1,
29103          rosTvOutputClass: (state.tv.videoVisible===false?'is-video-off ':'')+(this._rosganTvAirSlots?.length?'is-multi ':'')+((state.tv.nameStraps||[]).some(p=>p.id===state.tv.activeNameStrap)&&state.tv.visible!==false?'has-name-strap':''),
29104          rosNameDraft:s.rosNameDraft||'',rosRoleDraft:s.rosRoleDraft||'',rosNameChange:e=>this.setRosganNameDraft('rosNameDraft',e.target.value),rosRoleChange:e=>this.setRosganNameDraft('rosRoleDraft',e.target.value),
29105          rosNameSave:()=>{const name=String(this.state.rosNameDraft||'').trim();if(!name){this.showToast('Ingresá un nombre.');return;}const tv=this.loadRosganOpsState().tv;const rows=tv.nameStraps||[];const id=this.state.rosNameEditId||crypto.randomUUID();if(rows.length>=50&&!rows.some(p=>p.id===id)){this.showToast('Podés guardar hasta 50 nombres.');return;}const person={id,name:name.slice(0,100),role:String(this.state.rosRoleDraft||'').trim().slice(0,120)};this.setRosganTvConfig({nameStraps:rows.some(p=>p.id===id)?rows.map(p=>p.id===id?person:p):[...rows,person]});this.setState({rosNameDraft:'',rosRoleDraft:'',rosNameEditId:''});},
29106          rosNames:(state.tv.nameStraps||[]).map(p=>({...p,active:p.id===state.tv.activeNameStrap,action:p.id===state.tv.activeNameStrap?'Quitar del aire':'Poner al aire',toggle:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({activeNameStrap:tv.activeNameStrap===p.id?'':p.id});},edit:()=>this.setState({rosNameDraft:p.name,rosRoleDraft:p.role,rosNameEditId:p.id}),remove:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({nameStraps:(tv.nameStraps||[]).filter(r=>r.id!==p.id),activeNameStrap:tv.activeNameStrap===p.id?'':tv.activeNameStrap});}})),
29107          rosWaitActive:state.tv.fullScreenPlate==='waiting',rosEndActive:state.tv.fullScreenPlate==='end',rosWaitClass:state.tv.fullScreenPlate==='waiting'?'is-on':'',rosEndClass:state.tv.fullScreenPlate==='end'?'is-on':'',
29108          rosWaitToggle:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({fullScreenPlate:tv.fullScreenPlate==='waiting'?'':'waiting'});},rosEndToggle:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({fullScreenPlate:tv.fullScreenPlate==='end'?'':'end'});},
29109          rosPlateVisible:state.tv.visible!==false&&['waiting','end'].includes(state.tv.fullScreenPlate),rosPlateTitle:this.state.currentRemate?.titulo||this.state.currentRemate?.nombre_remate||'Remate Rosgan',rosPlateStart:this.remateDate(this.state.currentRemate)?.getTime()||0,rosPlateDate:this.remateDate(this.state.currentRemate)?.toLocaleStr
29109ing('es-AR',{dateStyle:'long',timeStyle:'short'})||'Fecha a confirmar',rosPlateLocation:this.state.currentRemate?.ubicacion_remate||this.state.currentRemate?.lugar||this.state.currentRemate?.localidad||'Lugar a confirmar',rosPlateLogos:this.lotDestinationLogos(this.state.currentRemate||{}),
29110          ...this.rosganNameStrapPresentation(state.tv),
29111          rosTvShowSingleOverlay: state.tv.videoVisible===false || (!!selected && !this._rosganTvAirSlots?.length && !!this._rosganTvDecks?.onAirCert),
29112          rosTvAirPanes: this.rosganTvAirPanes(state.tv),
29113          rosTvLayoutLabel: ({ 2:'Doble · 2 lotes', 3:'Triple · 3 lotes', 4:'Cuádruple · 4 lotes' })[tvSlots.length] || 'Simple · 1 lote',
29114          rosTvClearSlots: () => this.setRosganTvConfig({ extraSlots:['','',''] }),
29115          rosTvLotList: s.route==='rosganTvConfig'?this.rosganTvCatalogRows(rows,state.tv,selectedCert,s.rosganTvQuery||''):[],
29116          rosTvQuery: s.rosganTvQuery || '',
29117          rosTvSearch: (event) => this.setState({ rosganTvQuery: event.target.value }),
29118          rosTvEditorFields: s.route === 'rosganTvConfig' ?this.rosganTvEditorFields(selected) : [],
29119          rosTvEditStatus: editorEntry?.status || 'Editá los datos y presioná guardar para actualizar TV.',
29120          rosTvSaveDisabled: !editorEntry || editorEntry.saving || !Object.keys(editorEntry.changes).length,
29121          rosTvSave: () => editorEntry && this.saveRosganTvLotEdits(editorEntry),
29122          rosTvVmixHost: s.rosganVmixDraft?.id===this.remateRouteId(remate)?s.rosganVmixDraft.host:(state.tv.vmixHost||'192.168.122.28:8088'),
29123          rosTvVmixInput: s.rosganVmixDraft?.id===this.remateRouteId(remate)?s.rosganVmixDraft.input:(state.tv.vmixBrowserInputNumber||''),
29124          rosTvVmixHostChange:event=>this.editRosganVmixConnection('host',event.target.value),
29125          rosTvVmixInputChange:event=>this.editRosganVmixConnection('input',event.target.value),
29126          rosTvVmixSave:()=>this.saveRosganVmixConnection(),
29127          rosTvVmixCopyUrl:()=>this.copyRosganVmixUrl(),
29128          rosTvVmixNumber: Number(state.tv.vmixBrowserInputNumber) || 'Sin vincular',
29129          rosTvSelect: (event) => this.selectRosganTvLot(event),
29130          rosTvPrev: () => this.stepRosganTvLot(-1),
29131          rosTvNext: () => this.stepRosganTvLot(1),
29132          rosTvVisible: state.tv.visible !== false,
29133          rosTvVisibleClass: state.tv.visible !== false ?'is-on' : '',
29134          rosTvVideoClass:state.tv.videoVisible!==false?'is-on':'',rosTvVideoVisible:state.tv.videoVisible!==false,rosTvToggleVideo:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({videoVisible:tv.videoVisible===false});},
29135          rosTvToggleVisible: () => this.setRosganTvConfig({ visible: state.tv.visible === false }),
29136          rosTvLogosVisible: state.tv.logosVisible !== false,
29137          rosTvLogosClass: state.tv.logosVisible !== false ?'is-on' : '',
29138          rosTvToggleLogos: () => this.setRosganTvConfig({ logosVisible: state.tv.logosVisible === false }),
29139          rosTvLowerVisible: state.tv.lowerThirdVisible !== false,
29140          rosTvLowerClass: state.tv.lowerThirdVisible !== false ?'is-on' : '',
29141          rosTvToggleLower: () => this.setRosganTvConfig({ lowerThirdVisible: state.tv.lowerThirdVisible === false }),
29142          rosTvContactsVisible: state.tv.contactsVisible !== false,
29143          rosTvContactsClass: state.tv.contactsVisible !== false ?'is-on' : '',
29144          rosTvToggleContacts: () => this.setRosganTvConfig({ contactsVisible: state.tv.contactsVisible === false }),
29145          rosTvContacts: consContacts,
29146          rosTvContactRows: s.route === 'rosganTvConfig' ?Array.from(new Map([
29147            ...rows.map(row => row.consignataria),
29148            ...(remate.consignatariasParticipantes || []).map(participant => {
29149              const record=(s.consignatarias || []).find(item => String(item.id) === String(participant.consignatariaId || participant.id));
29150              return this.consDisplayName(participant.consignatariaNombre || participant.nombre || record?.name || record?.nombre || '');
29151            }),
29152          ].filter(Boolean).map(name => [this.consKey(name),name])).entries()).map(([key,name]) => {
29153            const record=(s.consignatarias || []).find(item => this.consKey(item.name || item.nombre) === key);
29154            const raw=record?.telefonos || record?.phones || record?.telefono || record?.phone || '';
29155            const value=state.tv.contactsByConsignataria?.[key] ?? (Array.isArray(raw) ?raw.map(item => typeof item === 'string' ?item :item.phone || item.telefono || '').filter(Boolean).join(' | ') :String(raw));
29156            const logo=this.consWhiteLogo(name) || this.consLogo(name);
29157            return {key,name,value,logo,hasLogo:!!logo,noLogo:!logo,initial:name.charAt(0),color:this.consBrandColor(name),label:'Contactos de ' + name,
29158              change:event => this.setRosganTvConfig({contactsByConsignataria:{...this.loadRosganOpsState().tv.contactsByConsignataria,[key]:event.target.value}})};
29159          }) :[],
29160          rosTvOutputUrl: this.remateSectionPath(remate, 'tv'),
29161          rosTvOpenOutput: () => window.open(this.remateSectionPath(remate, 'tv'), '_blank', 'noopener'),
29162          rosTvHasLot: !!selected,
29163          rosTvNoLot: !selected && !s.remateLotsLoading,
29164          rosTvVideoUrl: videoUrl,
29165          rosTvHasVideo: !!videoUrl,
29166          rosTvNoVideo: !!selected && !videoUrl,
29167          rosTvPoster: poster,
29168          rosTvPlay: (event) => this.playRosganTvVideo(event),
29169          rosTvLotNumber: this.rosganTvLotNumber(selected),
29170          rosTvCert: selected ?String(selected.nro_certificado || selected.id_certificado || '—') : '',
29171          rosTvCategory: this.rosganTvCategory(selected),
29172          rosTvPregnancy: this.rosganTvFixedPregnancy(selected),
29173          rosTvShowVisec: state.tv.visible!==false&&state.tv.lowerThirdVisible!==false&&this.rosganTvIsAptoVisec(selected),
29174          rosTvHasExtraFields: extraFields.length > 0,
29175          rosTvExtraLabel: extraFields[extraIndex]?.label || '',
29176          rosTvExtraValue: extraFields[extraIndex]?.value || '',
29177          rosTvObservations: tvObservations,
29178          rosTvShowChina:state.tv.visible!==false&&state.tv.lowerThirdVisible!==false&&this.rosganTvIsAptoChina(selected),
29179          rosTvHasObservations: !!tvObservations && !/^(?:\?|[-—–]|null|sin datos?)$/i.test(tvObservations),
29180          rosTvObservationsDuration: Math.max(18, tvObservations.length / 8),
29181          rosTvBreed: selected?.raza || selected?.nombre_raza || '',
29182          rosTvHasQuantity: !!this.rosganTvQuantity(selected),
29183          rosTvHasWeight: Number(selected?.peso_promedio) > 0,
29184          rosTvPayment: selected?.plazo || '',
29185          rosTvTick: selected?.garrapata || '',
29186          rosTvQuantity: this.rosganTvQuantity(selected),
29187          rosTvQuantityFirst: this.rosganTvQuantityBreakdown(selected).split('\n')[0],
29188          rosTvQuantitySecond: this.rosganTvQuantityBreakdown(selected).split('\n')[1] || '',
29189          rosTvWeight: selected && Number(selected.peso_promedio) ?numberText(selected.peso_promedio) + ' kg' : '',
29190          rosTvLocation: selectedRow?.locality || '',
29191          rosTvConditions: conditions || 'Condiciones a confirmar',
29192          rosTvConsignataria: selectedConsignataria,
29193          rosTvConsLogo: consLogo,
29194          rosTvConsFullLogo: this.consFullHeroLogo(selectedConsignataria),
29195          rosTvHasConsLogo: !!consLogo,
29196          rosTvNoConsLogo: !consLogo,
29197          rosTvConsInitial: selectedConsignataria.charAt(0).toUpperCase(),
29198          rosTvConsColor: temporarySelectedConsignataria?.color || this.consBrandColor(selectedConsignataria),
29199          rosTvShowBrandBug: state.tv.visible !== false && state.tv.logosVisible !== false,
29200          rosTvShowLower: state.tv.visible !== false && state.tv.lowerThirdVisible !== false,
29201          rosTvShowContacts: state.tv.visible !== false && state.tv.lowerThirdVisible !== false && state.tv.contactsVisible !== false && !!consContacts,
29202        };
29203      })(),
29204      ...(() => {
29205        const cabanas = Array.isArray(s.cabanas) ?s.cabanas : [];
29206        const breeds = [...new Set(cabanas.map((item) =>
29206 this.cabanaBreedOf(item)).filter(Boolean))];
29207        const visibleCabanas = s.cabanaBreed ?cabanas.filter((item) => this.cabanaBreedOf(item) === s.cabanaBreed) : cabanas;
29208        const dateLabel = (item) => {
29209          const date = this.cabanaAuctionStartAt(item);
29210          return date ? date.toLocaleDateString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',weekday:'short',day:'2-digit',month:'short',year:'numeric'}) : 'Fecha a confirmar';
29211        };
29212        const timeLabel = (item) => {
29213          const date = this.cabanaAuctionStartAt(item);
29214          return date ? date.toLocaleTimeString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}) + ' h' : '';
29215        };
29216        const mediaReady = (event) => {
29217          if (event?.currentTarget) event.currentTarget.dataset.dcLoaded = 'true';
29218        };
29219        const mediaError = (event) => {
29220          if (event?.currentTarget) event.currentTarget.style.display = 'none';
29221        };
29222        const cabanaCards = [...visibleCabanas].sort((a,b) => this.compareCabanaListing(a,b)).map((item) => {
29223          const mode = this.cabanaMode(item);
29224          const media = this.cabanaMedia(item);
29225          const participants = this.cabanaRemateParticipants(item);
29226          const mainCabana = participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
29227          const participantLogos = [...participants].sort((a,b) => Number(b.type === 'principal') - Number(a.type === 'principal')).map(cabana => {
29228            const src = cabana.logoDataUrl || '';
29229            const hasLogo = !!src && !s.cabanaCardLogoFailed?.[src];
29230            return { src, name: cabana.name, hasLogo, noLogo: !hasLogo, kind: cabana.type === 'principal' ? 'is-principal' : 'is-guest',
29231              onError: () => this.setState({cabanaCardLogoFailed:{...this.state.cabanaCardLogoFailed,[src]:true}}) };
29232          });
29233          const consRef = item?.consignatariaId || item?.consignataria || item?.nombre_cuenta || '';
29234          const consignatariaRecord = rawConsignatarias.find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(consRef)));
29235          const consignatariaName = (consignatariaRecord ?this.consName(consignatariaRecord) : '') || (String(consRef).includes('-') ?this.humanizeSlug(consRef) : consRef) || 'Rosgan';
29236          const whiteConsIcon = this.consWhiteLogo(consRef || consignatariaName);
29237          const consIcon = whiteConsIcon || this.consLogo(consRef || consignatariaName) || this.consLogo(consignatariaName);
29238          const mainLogo = mainCabana.logoDataUrl || '';
29239          const listFallbackLogo = mainLogo || participants.find(c => c.logoDataUrl)?.logoDataUrl || this.consLogo(consRef || consignatariaName) || this.consLogo(consignatariaName) || consIcon || 'assets/rosgan-logo-blue.png';
29240          const participantNames = participants.map(c => c.name).filter(Boolean).join(' · ');
29241          const participantRoll = participants.length > 3 || participantNames.length > 85;
29242          const cardId = this.cabanaRemateId(item);
29243          const cardMedia = s.cabanaCardMedia?.[cardId] || { ready: false, frames: [] };
29244          const heroFrames = Array.isArray(cardMedia.frames) ?cardMedia.frames : [];
29245          return {
29246            createLot: (event) => { event?.stopPropagation?.(); this.openCabanaLotCreate(item); }, canCreateLot: this.canCreateCabanaLot(item),
29247            edit: (event) => { event?.stopPropagation?.(); this.openCabanaRemateEdit(item); }, canEdit: this.canEditCabanaRemate(item),
29248            salesOrder: (event) => { event?.stopPropagation?.(); this.openCabana(item, { openSalesOrder: true }); }, canManageSalesOrder: this.canManageUsers(),
29249            openRow: event => { if (event?.target?.closest?.('button, a, input, select, textarea, [role=button]')) return; this.openCabana(item); },
29250            open: () => this.openCabana(item), title: item.nombre_remate || item.title || item.titulo || 'Remate de cabaña',
29251            breed: this.cabanaBreedOf(item) || 'Reproductores', color: this.cabanaColor(item), brandDark: this.cabanaBrandDark(item),
29252            media, hasMedia: !!media, noMedia: !media, mediaReady, mediaError, initial: String(item.nombre_remate || item.title || item.titulo || 'C').replace(/^\d+[^A-Za-z0-9]*/i, '').trim().charAt(0).toUpperCase() || 'C',
29253            listThumb: heroFrames[0]?.src || media || listFallbackLogo, listThumbClass: heroFrames[0]?.src || media ? 'has-photo' : '', participantNames, participantRoll, participantRollClass: participantRoll ? 'is-rolling' : '', participantRollDuration: Math.max(22, participantNames.length * .22) + 's',
29254            toggleListRoll: event => { const button=event.currentTarget; const paused=button.closest('.cabana-list-participants').classList.toggle('is-paused'); button.setAttribute('aria-pressed', String(paused)); button.setAttribute('aria-label', paused ? 'Reanudar desplazamiento de participantes' : 'Pausar desplazamiento de participantes'); button.textContent=paused ? '▷' : 'Ⅱ'; },
29255            listThumbError: event => { const img=event.currentTarget; if(img.dataset.fallback)return; img.dataset.fallback='true';img.src=listFallbackLogo;img.closest('button')?.classList.remove('has-photo'); },
29256            heroFrames, heroBase: heroFrames[0]?.src || '', hasHeroMedia: heroFrames.length > 0, noHeroMedia: heroFrames.length === 0, fallbackClass: cardMedia.ready ?'' : 'is-loading', fallbackLogo: mainLogo || 'assets/rosgan-logo-blue.png',
29257            prebidsActive: window.CabanaPrebid.isOpen(item),
29258            badge: mode === 'live' ?'EN VIVO' : mode === 'upcoming' ?'PRÓXIMO' : 'FINALIZADO',
29259            badgeBg: mode === 'live' ?'#e63946' : mode === 'upcoming' ?'#0a5ca8' : '#65758a',
29260            location: item.ubicacion_remate || item.lugar || item.localidad || item.provincia || item?.location?.lugar || item?.location?.localidad || item?.location?.provinciaNombre || 'Ubicación a confirmar',
29261            rollParticipants: participantLogos.length > 5, participantsRollClass: participantLogos.length > 5 ?'is-rolling' : '', participantsTabindex: participantLogos.length > 5 ?'0' : '-1', participantsDuration: Math.max(24, participantLogos.length * 4) + 's',
29262            toggleParticipantsRoll: event => { event.stopPropagation(); const button = event.currentTarget; const paused = button.closest('.cabana-card-brand-strip').classList.toggle('is-paused'); button.textContent = paused ?'Reanudar' : 'Pausar'; button.setAttribute('aria-label', paused ?'Reanudar movimiento de participantes' : 'Pausar movimiento de participantes'); },
29263            mainLogo, hasMainLogo: !!mainLogo, mainLogoAlt: mainCabana.name || 'Cabaña principal', participantLogos, hasLogos: participantLogos.length > 0,
29264            consignataria: consignatariaName, consBg: this.consBrandColor(consRef || consignatariaName), consIcon, hasConsIcon: !!consIcon, consIconFilter: whiteConsIcon ?'none' : 'brightness(0) invert(1)',
29265            date: dateLabel(item), time: timeLabel(item), dateTime: [dateLabel(item), timeLabel(item)].filter(Boolean).join(' · '),
29266            lots: Number(item.cantidad_lotes || item.lotsCount || item.lotCount || 0),
29267          };
29268        });
29269        const current = s.currentCabana;
29270        const currentHeroColor = current ?this.cabanaColor(current) : '#0a5ca8';
29271        const currentHeroColorMatch = currentHeroColor.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
29272        const currentHeroIsDark = !currentHeroColorMatch || ((parseInt(currentHeroColorMatch[1], 16) * .299) + (parseInt(currentHeroColorMatch[2], 16) * .587) + (parseInt(currentHeroColorMatch[3], 16) * .114)) < 158;
29273        const mapBrandLogo = (cabana) => {
29274          const whiteLogo = cabana?.logoWhiteDataUrl || '';
29275          const colorLogo = cabana?.logoDataUrl || '';
29276          const candidate = currentHeroIsDark ?(whiteLogo || colorLogo) : (colorLogo || whiteLogo);
29277          const logo = s.cabanaHeroLogoFailed?.[candidate] ? '' : candidate;
29278          return { onError: () => this.setState({cabanaHeroLogoFailed:{...this.state.cabanaHeroLogoFailed,[candidate]:true}}), name: cabana?.nombre || cabana?.name || 'Cabaña', logo, whiteLogo, filter: currentHeroIsDark && !whiteLogo && colorLogo ?'brightness(0) invert(1)' : 'none', hasLogo: !!logo, noLogo: !logo };
29279        };
29280        const mapGuestLogo = (cabana) => {
29281          const whiteLogo = cabana?.logoWhiteDataUrl || '';
29282          const colorLogo = cabana?.logoDataUrl || '';
29283          const candidate = currentHeroIsDark ?(whiteLogo || colorLogo) : (colorLogo || whiteLogo);
29284          const logo = s.cabanaHeroLogoFailed?.[candidate] ? '' : candidate;
29285          return { onError: () => this.setState({cabanaHeroLogoFailed:{...this.state.cabanaHeroLogoFailed,[candidate]:true}}), name: cabana?.nombre || cabana?.name || 'Cabaña', logo, whiteLogo, filter: currentHeroIsDark && !whiteLogo && colorLogo ?'brightness(0) invert(1)' : 'none', hasLogo: !!logo, noLogo: !logo };
29286        };
29287        const participants = this.cabanaRemateParticipants(current || {});
29288        const mainBrandLogos = participants.filter((cabana) => cabana.type === 'principal').map(mapBrandLogo);
29289        const guestBrandLogos = participants.filter((cabana) => cabana.type !== 'principal').map(mapGuestLogo);
29290        const primaryWhiteLogo = participants.find((cabana) => cabana.type === 'principal')?.logoWhiteDataUrl || '';
29291        const currentConsignatariaRecord = current ?rawConsignatarias.find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(current?.consignatariaId || current?.consignataria || current?.nombre_cuenta))) : null;
29292        const currentConsignatariaRaw = current ?(current.consignataria || current.nombre_cuenta || current?.consignatariaId || '') : '';
29293        const currentConsignatariaName = current ?((currentConsignatariaRecord ?this.consName(currentConsignatariaRecord) : '') || (String(currentConsignatariaRaw).includes('-') ?this.humanizeSlug(currentConsignatariaRaw) : currentConsignatariaRaw) || 'Rosgan') : '';
29294        const currentConsignatariaLogo = current ?(this.consLogo(currentConsignatariaName) || this.consLogo(current?.consignatariaId) || (currentConsignatariaRecord && (currentConsignatariaRecord.logoUrl || currentConsignatariaRecord.logo || currentConsignatariaRecord.imageUrl))) : '';
29295        const currentConsignatariaWhiteLogo = current ?this.consWhiteLogo(currentConsignatariaRaw || currentConsignatariaName) : '';
29296        const currentConsignatariaColor = current ?this.consBrandColor(currentConsignatariaRaw || currentConsignatariaName) : '#002e5f';
29297        const currentContact = this.cabanaContactData(current);
29298        const prebidPanelData = s.cabanaPrebidPanelData || {};
29299        const prebidPanelQuery = String(s.cabanaPrebidPanelQuery || '').trim().toLowerCase();
29300        const prebidPanelStatus = String(s.cabanaPrebidPanelStatus || 'all');
29301        const prebidPanelRawItems = Array.isArray(prebidPanelData.items) ?prebidPanelData.items : [];
29302        const prebidPanelLotBase = this.cabanaPrebidPanelLots(prebidPanelData);
29303        const requestedPanelLot = String(s.cabanaPrebidPanelLot || '');
29304        const prebidPanelSelectedLot = prebidPanelLotBase.find((lot) => lot.key === requestedPanelLot) || prebidPanelLotBase[0] || null;
29305        const prebidPanelSelectedLotKey = String(prebidPanelSelectedLot?.key || '');
29306        const prebidPanelScopedItems = prebidPanelRawItems.filter((item) => String(item?.lotCert || '') === prebidPanelSelectedLotKey);
29307        const prebidPanelLots = prebidPanelLotBase.map((lot) => ({
29308          ...lot,
29309          className: 'cb-prebid-lot-option' + (lot.key === prebidPanelSelectedLotKey ?' is-active' : ''),
29310          go: () => this.selectCabanaPrebidPanelLot(lot.key),
29311        }));
29312        const prebidPanelItems = prebidPanelScopedItems.filter((item) => {
29313          const normalizedStatus = item?.isCurrent ?'current' : (/pendiente|pe
29313nding/i.test(String(item?.status || '')) ?'pending' : 'outbid');
29314          if (prebidPanelStatus !== 'all' && normalizedStatus !== prebidPanelStatus) return false;
29315          if (!prebidPanelQuery) return true;
29316          const animalText = (Array.isArray(item?.animals) ?item.animals : []).flatMap((animal) => [animal?.cert, ...(Array.isArray(animal?.fields) ?animal.fields.flatMap((field) => [field?.label, field?.value]) : [])]);
29317          return [item?.buyer?.name, item?.buyer?.email, item?.buyer?.cuitCuil, item?.lotLabel, item?.lotCert, item?.category, item?.cabanaName, item?.enComisionPara, ...animalText].filter(Boolean).join(' ').toLowerCase().includes(prebidPanelQuery);
29318        });
29319        const prebidPanelRows = prebidPanelItems.map((item) => {
29320          const created = item?.createdAt ?new Date(item.createdAt) : null;
29321          const statusKey = item?.isCurrent ?'current' : (/pendiente|pending/i.test(String(item?.status || '')) ?'pending' : 'outbid');
29322          const animals = (Array.isArray(item?.animals) ?item.animals : []).map((animal) => {
29323            const fields = (Array.isArray(animal?.fields) ?animal.fields : []).filter((field) => field?.value != null && String(field.value).trim());
29324            return {
29325              cert: animal?.cert ?(animal.identifierName || 'Cert.')+' '+animal.cert : '',
29326              detail: fields.map((field) => field.label + ': ' + field.value).join(' · ') || 'Sin identificación adicional',
29327            };
29328          });
29329          return {
29330            id: item?.id || [item?.lotCert, item?.amount, item?.createdAt].join('-'),
29331            amount: this.fmtARS(Number(item?.amount || 0)),
29332            date: created && !Number.isNaN(created.getTime()) ?created.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'Sin fecha',
29333            status: item.status || 'Sin estado',
29334            canApprove: /pendienteaprobacion/i.test(item.status),
29335            busy: !!s.cabanaPrebidApprovalBusy,
29336            approve: () => this.resolveCabanaPrebid(item,'approve'),
29337            reject: () => this.resolveCabanaPrebid(item,'reject'),
29338            canDelete:this.canDeleteCabanaPrebid(),deleteOffer:()=>
29338this.deleteCabanaPrebid(item),deleteBusy:!!s.cabanaPrebidDeleting,
29339            statusClass: 'cb-prebid-admin-status is-' + statusKey,
29340            buyerName: item?.buyer?.name || 'Usuario sin nombre',
29341            buyerEmail: item?.buyer?.email || 'Sin email',
29342            buyerHref: item?.buyer?.email ?'mailto:' + item.buyer.email : '',
29343            buyerCuit: item?.buyer?.cuitCuil || 'CUIT/CUIL no informado',
29344            lot: item?.lotLabel || ('Lote ' + (item?.lotCert || '')),
29345            lotMeta: [item?.category, item?.cabanaName].filter(Boolean).join(' · '),
29346            commission: item?.enComisionPara ?'En comisión para ' + item.enComisionPara : '',
29347            hasCommission: !!item?.enComisionPara,
29348            animals,
29349            hasAnimals: animals.length > 0,
29350          };
29351        });
29352        const prebidPanelBidderKeys = new Set(prebidPanelScopedItems.map((item) => item?.buyer?.uid || item?.buyer?.email).filter(Boolean));
29353        const prebidPanelCurrentItem = prebidPanelScopedItems.filter(item=>window.CabanaPrebid.accepted(item)).sort((a,b)=>Number(b.amount)-Number(a.amount))[0] || null;
29354        const prebidPanelLastDate = prebidPanelScopedItems.reduce((latest, item) => {
29355          const value = item?.createdAt ?new Date(item.createdAt).getTime() : 0;
29356          return Number.isFinite(value) && value > latest ?value : latest;
29357        }, 0);
29358        const prebidPanelMetrics = [
29359          { label: 'Preofertas del lote', value: prebidPanelScopedItems.length.toLocaleString('es-AR') },
29360          { label: 'Ofertantes', value: prebidPanelBidderKeys.size.toLocaleString('es-AR') },
29361          { label: 'Oferta actual', value: prebidPanelCurrentItem ?this.fmtARS(Number(prebidPanelCurrentItem.amount || 0)) : 'Sin ofertas' },
29362          { label: 'Última actividad', value: prebidPanelLastDate ?new Date(prebidPanelLastDate).toLocaleString('es-AR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : 'Sin actividad' },
29363        ];
29364        const prebidPanelUpdatedDate = prebidPanelData.updatedAt ?new Date(prebidPanelData.updatedAt) : null;
29365        const prebidPanelUpdated = prebidPanelUpdatedDate && !Number.isNaN(prebidPanelUpdatedDate.getTime()) ?'Actualizado ' + prebidPanelUpdatedDate.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : 'Actualización automática';
29366        const lots = this.cabanaSortLotsByCategoryOrder(s.cabanaLots || []);
29367        const lotText = (lot, keys, fallback = '') => {
29368          for (const key of keys) {
29369            const value = lot?.[key]  || lot?.values?.[key]  || lot?.subformValues?.datos_principales?.[key];
29370            if (value !== undefined && value !== null && value !== '') return value;
29371          }
29372          return fallback;
29373        };
29374        const lotCategory = (lot) => String(lotText(lot, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], 'Reproductor'));
29375        const auditorRows = this.canAuditCabana(current) && s.route === 'cabanaAuditor' ? [...new Map(lots.map(lot => [this.cabanaPrebidCert(lot), lot])).values()].filter(lot => this.canEditCabanaLot(current, lot) && [this.cabanaLotNumber(lot), lotText(lot, ['rp', 'RP']), lotText(lot, ['nombre', 'lotName']), lotCategory(lot)].join(' ').toLowerCase().includes(String(s.cabanaAuditorQuery || '').trim().toLowerCase())).map(lot => {
29376          const key = this.cabanaRemateId(current) + ':' + this.cabanaPrebidCert(lot);
29377          this._cabanaAuditorDrafts ||= {};
29378          const draft = this._cabanaAuditorDrafts[key] || {base:lot.prebidBaseOverride ?? '', increment:lot.prebidIncrementOverride ?? ''};
29379          const pricing = window.CabanaPrebid.pricing(lot,current);
29380          const upload = s.cabanaCardVideoUploads?.[this.cabanaLotId(lot)] || {};
29381          return {title:'Lote ' + this.cabanaLotNumber(lot) + ' · RP ' + lotText(lot,['rp','RP'],'—'), subtitle:lotText(lot,['nombre','lotName']) + ' · ' + lotCategory(lot), base:draft.base, increment:draft.increment,
29382            effective:'Vigente: base ' + (pricing.base == null ? 'Sin configurar' : this.fmtARS(pricing.base)) + ' · incremento ' + (pricing.increment == null ? 'Sin configurar' : this.fmtARS(pricing.increment)),
29383            setBase:event => {this._cabanaAuditorDrafts[key] = {...draft, ...this._cabanaAuditorDrafts[key], base:event.target.value};},
29384            setIncrement:event => {this._cabanaAuditorDrafts[key] = {...draft, ...this._cabanaAuditorDrafts[key], increment:event.target.value};},
29385            saving:!!s.cabanaAuditorSaving?.[key], saveLabel:s.cabanaAuditorSaving?.[key] ? 'Guardando…' : 'Guardar', save:event =>
29385 this.saveCabanaAuditorPricing(current,lot,event),
29386            edit:() => this.openCabanaLotEdit(current,lot), canUpload:this.canUploadCabanaLotMedia(current,lot), canFinal:this.canUploadCabanaFinal(current), uploading:!!upload.busy, upload:event => this.quickUploadCabanaLotVideo(event,lot), status:upload.busy ? (upload.stage || 'Subiendo…') + ' ' + Math.round(upload.progress || 0) + '%' : ''};
29387        }) : [];
29388        const isToro = (lot) => /toro|macho|mej/i.test(lotCategory(lot));
29389        const query = String(s.cbQuery || '').trim().toLowerCase();
29390        const filteredLots = lots.filter((lot) => (!query || [lotCategory(lot), this.cabanaLotNumber(lot), lotText(lot, ['rp', 'RP', 'numero_rp', 'caravana', 'lote']), lotText(lot, ['nombre', 'lotName', 'nombre_lote']), lotText(lot, ['raza', 'razaNombre']), lot?.cabanaNombre].join(' ').toLowerCase().includes(query)));
29391        const groupLots = (source) => {
29392          const groups = new Map();
29393          (Array.isArray(source) ?source : []).forEach((lot, index) => {
29394            const key = this.cabanaLotGroupKey(lot, index);
29395            if (!groups.has(key)) groups.set(key, {
29396              key,
29397              categoryName: lotCategory(lot),
29398              categoryOrder: this.cabanaCategoryDisplayOrder(lot),
29399              lotNumber: this.cabanaLotNumber(lot),
29400              variants: [],
29401            });
29402            groups.get(key).variants.push(lot);
29403          });
29404          return Array.from(groups.values())
29405            .map((group) => ({
29406              ...group,
29407              variants: group.variants.sort((a, b) =>
29408                this.cabanaLotSortNumber(a?.itemDisplayOrder, 0) -
29409                this.cabanaLotSortNumber(b?.itemDisplayOrder, 0)
29410              ),
29411            }))
29412            .sort((a, b) => {
29413              return String(a.lotNumber).localeCompare(String(b.lotNumber), 'es', {
29414                sensitivity: 'base',
29415                numeric: true,
29416              });
29417            });
29418        };
29419        const allLotGroups = groupLots(lots);
29420        const filteredGroups = groupLots(filteredLots);
29421        const analyticsRoot = s.cabanaAnalyticsSummary?.data || s.cabanaAnalyticsSummary || {};
29422        const analyticsLots = this.listFrom(analyticsRoot, ['lots', 'items', 'data']).filter(Boolean);
29423        const analyticsRps = this.listFrom(analyticsRoot, ['rps', 'items', 'data']).filter(Boolean);
29424        const analyticsScore = (row) => Number(row?.rpViews || row?.views || row?.viewCount || 0) + Number(row?.videoPlays || row?.plays || 0) + Number(row?.imageViews || 0) + Number(row?.contactClicks || row?.contacts || 0) + Number(row?.shares || row?.shareClicks || row?.event_share_click || 0);
29425        const topRpRankByKey = new Map();
29426        analyticsRps
29427          .map((row) => ({ row, score: analyticsScore(row), rp: String(row?.rp || row?.rpId || row?.itemId || '').trim() }))
29428          .filter((item) => item.rp && item.score > 0)
29429          .sort((a, b) => b.score - a.score)
29430          .slice(0, 3)
29431          .forEach((item, rankIndex) => {
29432            topRpRankByKey.set(this.consKey(item.rp), { rank: rankIndex + 1, score: item.score });
29433          });
29434        const rankBadgeForRp = (rpValue) => {
29435          const hit = topRpRankByKey.get(this.consKey(rpValue));
29436          if (!hit) return null;
29437          if (hit.rank === 1) return {
29438            label: 'Mas visto',
29439            bg: 'rgba(250,204,21,.94)',
29440            color: '#3b2500',
29441            border: 'rgba(255,244,178,.9)',
29442            iconUrl: 'assets/icons/lucide/flame.svg',
29443            title: `${hit.score.toLocaleString('es-AR')} senales registradas`,
29444          };
29445          if (hit.rank === 2) return {
29446            label: 'Segundo mas visto',
29447            bg: 'rgba(236,242,248,.94)',
29448            color: '#243548',
29449            border: 'rgba(255,255,255,.82)',
29450            iconUrl: 'assets/icons/lucide/medal.svg',
29451            title: `${hit.score.toLocaleString('es-AR')} senales registradas`,
29452          };
29453          return {
29454            label: 'Tercero mas visto',
29455            bg: 'rgba(190,126,74,.94)',
29456            color: '#fffaf2',
29457            border: 'rgba(255,222,186,.72)',
29458            iconUrl: 'assets/icons/lucide/medal.svg',
29459            title: `${hit.score.toLocaleString('es-AR')} senales registradas`,
29460          };
29461        };
29462        const prebidWindow = window.CabanaPrebid.windowState(current);
29463        const prebidActive = !!(current?.prebid?.habilitarPreofertas || current?.prebid?.habilitada || current?.prebid?.enabled || current?.prebid?.isActive || String(current?.preoferta || '').toLowerCase() === 'activa');
29464        const isEmptyValue = (value) => {
29465          const text = String(value || '').trim();
29466          return !text || text === '-' || /^[-\u2013\u2014]+$/.test(text) || text.toLowerCase() === 'null' || text.toLowerCase() === 'undefined';
29467        };
29468        const cleanLabel = (value) => String(value || '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
29469        const displayValue = (value) => {
29470          if (Array.isArray(value)) return value.map(displayValue).filter((item) => !isEmptyValue(item)).join(', ');
29471          if (value && typeof value === 'object') return value.name || value.nombre || value.label || value.value || '';
29472          if (typeof value === 'boolean') return value ?'Sí' : 'No';
29473          return value === undefined || value === null ?'' : String(value);
29474        };
29475        const cardFactsFromSchema = (lot) => this.cabanaLotOnCardFacts(lot, current, 4);
29476        const compactCardFact = (fact) => {
29477          const title = String(fact?.value || '').trim();
29478          if (title.length <= 48) return { ...fact, value: title, title };
29479          const dates = title.match(/\d{1,2}\/\d{1,2}\/\d{4}/g) || [];
29480          if (dates.length > 2) return { ...fact, value: dates.slice(0, 2).join(' · ') + ' +' + (dates.length - 2), title };
29481          const labelKey = this.consKey(fact?.label || '');
29482          const rpParts = title.split('-').map((part) => part.trim()).filter(Boolean);
29483          if (labelKey === 'rp' && rpParts.length > 2 && rpParts.every((part) => /^[a-z0-9]+$/i.test(part))) {
29484            return { ...fact, value: rpParts.slice(0, 2).join(' · ') + ' +' + (rpParts.length - 2), title };
29485          }
29486          return { ...fact, value: title.slice(0, 45).replace(/[\s-]+$/g, '') + '…', title };
29487        };
29488        const mapLot = (lot, index) => {
29489          const videos = this.cabanaLotVideos(lot);
29490          const itemCoverUrl = this.cabanaLotItemCoverUrl(lot);
29491          const firstVideo = videos[0] || {};
29492          const generatedVideoThumb = firstVideo.thumb || s.videoThumbByUrl[firstVideo.url] || '';
29493          const media = itemCoverUrl || this.cabanaLotFallbackCoverUrl(lot) || generatedVideoThumb || '';
29494          const videoPreview = media ?'' : (firstVideo.url || '');
29495          const cardUploadState = s.cabanaCardVideoUploads?.[this.cabanaLotId(lot)] || {};
29496          const rp = String(lotText(lot, ['rp', 'RP', 'numero_rp', 'lotNumber', 'orden_final'], index + 1));
29497          const identity = window.CabanaLotIdentity.resolve(lot, current);
29498          const lotNumber = identity.number;
29499          const category = lotCategory(lot);
29500          const statusRaw = String(lotText(lot, ['estado', 'auctionStatus', 'status'], 'Disponible')).toLowerCase();
29501          const sold = /vend|sold|closed/.test(statusRaw);
29502          const explicitName = String(lotText(lot, ['nombre', 'lotName', 'nombre_lote'], '')).trim();
29503          const generatedName = (lot?.cabanaNombre ?lot.cabanaNombre + ' - ' : '') + category + ' RP ' + rp;
29504          const duplicateName = explicitName && (this.consKey(explicitName) === this.consKey(generatedName) || (lot?.cabanaNombre && this.consKey(explicitName).includes(this.consKey(lot.cabanaNombre)) && this.consKey(explicitName).includes(this.consKey(category)) && this.consKey(explicitName).includes(this.consKey(rp))));
29505          const name = explicitName && !duplicateName ?explicitName : '';
29506          const breed = String(lotText(lot, ['razaNombre', 'raza', 'breedName'], this.cabanaBreedOf(current) || ''));
29507          const weight = Number(lotText(lot, ['peso', 'pesoPromedio', 'peso_promedio'], 0));
29508          const born = lotText(lot, ['nacimiento', 'anio_nacimiento', 'fecha_nacimiento'], '-');
29509          const base = window.CabanaPrebid.pricing(lot, current).base;
29510          const priceMap=s.cabanaCatalogPrices?.id===this.cabanaRemateId(current)?s.cabanaCatalogPrices.prices:{};
29511          const priceEntry=priceMap?.[this.cabanaPrebidCert(lot)];
29512          const offered=lot.itemId ? priceEntry?.items?.[String(lot.itemId)] : priceEntry?.amount;
29513          const hasOffer=offered!=null && Number.isFinite(Number(offered));
29514          const displayPrice=hasOffer?Number(offered):base;
29515          const hasBase = Number.isFinite(displayPrice) && displayPrice >= 0;
29516          const lotCabana = participants.find((cabana) => (lot?.cabanaKey && cabana.key === lot.cabanaKey) || (lot?.cabanaId && String(cabana.id || '') 
29516=== String(lot.cabanaId)) || (lot?.cabanaNombre && cabana.name === lot.cabanaNombre)) || participants[0] || {};
29517          const backgroundLogo = '';
29518          const fallbackFacts = [{ label: 'NACIMIENTO', value: String(born) }, { label: 'PESO', value: weight ?weight.toLocaleString('es-AR') + ' kg' : '-' }, { label: isToro(lot) ?'C. ESCROTAL' : 'REGISTRO', value: String(lotText(lot, isToro(lot) ?['ce', 'circunferenciaEscrotal'] : ['registro', 'hba'], '-')) }, { label: 'DEP DESTETE', value: String(lotText(lot, ['dep', 'depDestete'], '-')) }].filter((fact) => !isEmptyValue(fact.value));
29519          const facts = cardFactsFromSchema(lot);
29520          const rankBadge = rankBadgeForRp(rp);
29521          const rpParts = rp.split('-').map((part) => part.trim()).filter(Boolean);
29522          const isMultiRp = rpParts.length > 2 && rpParts.every((part) => /^[a-z0-9]+$/i.test(part));
29523          const rpLabel = identity.type === 'corral' && (!lot?.values?.rp && !lot?.values?.RP || isMultiRp) ? '' : 'RP ' + rp;
29524          return { raw: lot, open: () => this.openCabanaLot(lot), rp: rpLabel, rpTitle: 'RP ' + rp, lotLabel: identity.label, lotBadge: identity.label.toUpperCase() + ' ' + lotNumber, lotNumber: String(lotNumber), hasLotNumber: !isEmptyValue(lotNumber), name, showName: false, category, breed, media, hasMedia: !!media, videoPreview, hasVideoPreview: !!videoPreview, mediaReady, mediaError,
29525            canUploadVideo: !media && !videoPreview && !cardUploadState.busy && this.canUploadCabanaLotMedia(current, lot), uploadVideo: (event) => this.quickUploadCabanaLotVideo(event, lot), stopUploadClick: (event) => event.stopPropagation(), videoUploadBusy: !!cardUploadState.busy, videoUploadProgress: Math.max(0, Math.min(100, Number(cardUploadState.progress || 0))), videoUploadStage: cardUploadState.stage || 'Preparando…',
29526            hasRp: !!rpLabel, backgroundLogo, hasBackgroundLogo: false, backgroundLogoFilter: 'none',
29527            status: sold ?'Vendido' : 'Disponible', statusBg: sold ?'#65758a' : '#1f7a63', opacity: sold ?0.72 : 1,
29528            facts: (facts.length ?facts : fallbackFacts).map(compactCardFact),
29529            hasRankBadge: !!rankBadge, rankLabel: rankBadge?.label || '', rankIconUrl: rankBadge?.iconUrl || '', rankBg: rankBadge?.bg || '', rankColor: rankBadge?.color || '', rankBorder: rankBadge?.border || '', rankTitle: rankBadge?.title || '',
29530            base: hasBase ?this.fmtARS(displayPrice) : '', showBase: hasBase, priceLabel:hasOffer?'OFERTA ACTUAL':'BASE', priceColor:hasOffer?'#08765a':'#0b63af', priceWeight:hasOffer?'800':'400', videos,
29531          };
29532        };
29533        const carouselStep = Number(s.cbCarouselStep || 0);
29534        const carouselOffsets = s.cbCarouselOffsets || {};
29535        this._cabanaActiveCardByGroup = this._cabanaActiveCardByGroup || {};
29536        this._cabanaCarouselPreloadedMedia = this._cabanaCarouselPreloadedMedia || new Set();
29537        const lotCards = filteredGroups.map((group, index) => {
29538          const count = group.variants.length;
29539          const activeIndex = count ?((carouselStep + Number(carouselOffsets[group.key] || 0)) % count + count) % count : 0;
29540          const activeLot = group.variants[activeIndex];
29541          const variantCards = group.variants.map((variant, variantIndex) => mapLot(variant, variantIndex));
29542          variantCards.forEach((candidate) => {
29543            if (!candidate.hasMedia || !candidate.media || this._cabanaCarouselPreloadedMedia.has(candidate.media)) return;
29544            this._cabanaCarouselPreloadedMedia.add(candidate.media);
29545            const image = new Image();
29546            image.src = candidate.media;
29547            if (typeof image.decode === 'function') image.decode().catch(() => {});
29548          });
29549          const card = variantCards[activeIndex] || mapLot(activeLot, index);
29550          const previousCard = (s.cbCarouselPreviousMedia || {})[group.key] || null;
29551          const isCrossfading = count > 1 && s.cbCarouselFading && (s.cbCarouselFadingKey === '*' || s.cbCarouselFadingKey === group.key) && !!previousCard;
29552          const cardMediaReady = (event) => {
29553            mediaReady(event);
29554            if (isCrossfading) this.revealCabanaCarouselCrossfade();
29555          };
29556          this._cabanaActiveCardByGroup[group.key] = card;
29557          return {
29558            ...card,
29559            mediaReady: cardMediaReady,
29560            mediaOpacity: isCrossfading && !s.cbCarouselCrossfadeActive ?0 : 1,
29561            hasPreviousMediaLayer: isCrossfading && !!(previousCard?.hasMedia || previousCard?.hasBackgroun
29561dLogo),
29562            // Keep the previous image fully opaque while the next one fades in.
29563            // Fading both layers simultaneously exposes the blue fallback between them.
29564            previousMediaOpacity: 1,
29565            previousMedia: previousCard?.media || '',
29566            previousHasMedia: !!previousCard?.hasMedia,
29567            previousBackgroundLogo: previousCard?.backgroundLogo || '',
29568            previousHasBackgroundLogo: !!previousCard?.hasBackgroundLogo,
29569            previousBackgroundLogoFilter: previousCard?.backgroundLogoFilter || 'none',
29570            open: () => this.openCabanaLot(group.variants[0], group.variants),
29571            hasVariants: count > 1,
29572            hasCardMetaRow: count > 1 || !!card.hasRankBadge,
29573            variantPosition: (activeIndex + 1) + ' / ' + count,
29574            dots: group.variants.map((variant, dotIndex) => ({
29575              label: 'RP ' + String(variant?.rp || variant?.values?.rp || dotIndex + 1),
29576              select: (event) => this.selectCabanaCarouselSlide(group.key, dotIndex, event),
29577              bg: dotIndex === activeIndex ?(current ?this.cabanaColor(current) : '#0a5ca8') : '#c4d1df',
29578              width: dotIndex === activeIndex ?'25px' : '7px',
29579            })),
29580          };
29581        });
29582        const totalLotGroups = allLotGroups.length;
29583        const corralGroups = allLotGroups.filter(group => window.CabanaLotIdentity.resolve(group.variants[0], current).type === 'corral').length;
29584        const groupUnit = corralGroups && corralGroups === totalLotGroups ? 'corrales' : corralGroups ? 'lotes / corrales' : 'lotes';
29585        const selected = s.currentCabanaLot;
29586        const selectedMapped = selected ?mapLot(selected, 0) : null;
29587        const selectedPrebidWinner = this.cabanaWinningPrebid(s.cabanaPrebids);
29588        const selectedPrebidIncrement = this.cabanaPrebidIncrement(current);
29589        const selectedPrebidBase = this.cabanaPrebidBase(selected);
29590        const selectedInstallments=window.CabanaInstallments.resolve(current?.installmentConfig,s.currentCabanaLot);
29591        const selectedPrebidSuggested = this.cabanaPrebidMinimum();
29592        const selectedPrebidEndRaw = current?.prebid?.fechaHoraFinPreoferta || current?.prebid?.fin || current?.prebid?.endAt || current?.prebid?.endDate || '';
29593        const selectedPrebidEnd = selectedPrebidEndRaw ?new Date(selectedPrebidEndRaw) : null;
29594        const selectedOwnWinner = s.user?.uid && window.CabanaPrebid.ownWinner(s.cabanaPrebids,selected?.itemId,this.cabanaPrebidIdentifier(selected).value);
29595        const selectedPrebidWindowOpen = prebidActive && (!selectedPrebidEnd || Number.isNaN(selectedPrebidEnd.getTime()) || selectedPrebidEnd.getTime() > Date.now());
29596        const selectedPrebidForm = s.cabanaPrebidForm || { cuitCuil: '', amount: '', selectedIdentifiers: [], enComision: false, enComisionPara: '' };
29597        const selectedPrebidCerts = this.cabanaPrebidAvailableCerts();
29598        const selectedPrebidGrouped = this.cabanaPrebidGrouped(current);
29599        const selectedPrebidHistory = (Array.isArray(s.cabanaPrebids) ?[...s.cabanaPrebids] : [])
29600          .filter(prebid => !!s.user?.uid && prebid.isOwn === true)
29601          .sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0))
29602          .slice(0, 10)
29603          .map((prebid) => {
29604          const createdAt = prebid.createdAt ?new Date(prebid.createdAt) : null;
29605          const hasValidDate = createdAt && !Number.isNaN(createdAt.getTime());
29606          const pending = /pendiente|pending/i.test(prebid.status);
29607          const liveWinner = prebid.isWinner && selectedPrebidWindowOpen;
29608          const ownOutbid = !!prebid.isOwn && prebid.status === 'Superada' && selectedPrebidWindowOpen;
29609          const isBaseOffer = !prebid.isWinner && prebid.status === 'Superada' && Number(prebid.amount) === Number(selectedPrebidBase);
29610          const ownWinningAnimal = !!s.user?.uid && !!window.CabanaPrebid.ownWinner([prebid],selected?.itemId,this.cabanaPrebidIdentifier(selected).value);
29611          const displayStatus = ownWinningAnimal
29612            ?(selectedPrebidWindowOpen ?'Estás ganando' : 'Ganada')
29613            :isBaseOffer ?'Base' :((prebid.isWinner || ownOutbid) ?'Superada' : prebid.status);
29614          const statusIsOutbid = ownOutbid && !isBaseOffer;
29615          const statusIsWinning = ownWinningAnimal;
29616          const statusIsCurrent = false;
29617          return {
29618            id: prebid.id,
29619            canDelete:this.canDeleteCabanaPrebid(),deleteOffer:()=>this.deleteCabanaPrebid(prebid),deleteBusy:!!s.cabanaPrebidDeleting,
29620            amount: this.fmtARS(prebid.amount),
29621            status: displayStatus,
29622            date: hasValidDate ?createdAt.toLocaleString('es-AR', { dateStyle: 'short', timeStyle: 'short' }) : 'Fecha no informada',
29623            certs: prebid.targets?.length ?prebid.targets.map(t => (t.characteristicIdentifierName || 'Cert.') + ' ' + (t.characteristicIdentifierValue ?? t.cert) + (t.isWinning ?' · Ganadora':' · ' + (prebid.status === 'Ganadora' ?'Superada':prebid.status))).join(' / ') : prebid.certs.join(', '),
29624            commission: prebid.enComisionPara ?('En comisión para ' + prebid.enComisionPara) : '',
29625            hasCommission: false,
29626            statusBg: statusIsOutbid ?'#ffe9ec' : (statusIsWinning ?'#e6f6ef' : (statusIsCurrent ?'#0b5fae' : (pending ?'#fff5dc' : '#eef4fa'))),
29627            statusColor: statusIsOutbid ?'#c6283d' : (statusIsWinning ?'#087653' : (statusIsCurrent ?'#ffffff' : (pending ?'#9a6500' : '#31526f'))),
29628          };
29629        });
29630        const selectedVariants = Array.isArray(s.currentCabanaLotVariants) && s.currentCabanaLotVariants.length ?s.currentCabanaLotVariants : (selected ?[selected] : []);
29631        const selectedVariantId = this.cabanaLotVariantId(selected);
29632        const clRpVariants = selectedVariants.map((variant, index) => {
29633          const active = this.cabanaLotVariantId(variant) === selectedVariantId;
29634          const mapped = mapLot(variant, index);
29635          const rpValue = String(variant?.rp || variant?.values?.rp || mapped.rp || index + 1).replace(/^RP\s+/i, '');
29636          return {
29637            label: 'RP ' + rpValue,
29638            shortLabel: rpValue,
29639            thumb: this.cabanaLotItemCoverUrl(variant),
29640            hasThumb: !!this.cabanaLotItemCoverUrl(variant),
29641            noThumb: !this.cabanaLotItemCoverUrl(variant),
29642            active,
29643            className: 'dc-rp-variant-button' + (active ?' is-active' : ''),
29644            title: active ?('RP ' + rpValue + ' seleccionado') : ('Ver RP ' + rpValue),
29645            select: (event) => this.selectCabanaLotVariant(variant, event),
29646            border: active ?(current ?this.cabanaColor(current) : '#0a5ca8') : '#cad7e5',
29647            shadow: active ?'0 0 0 2px rgba(10,92,168,.18)' : 'none',
29648          };
29649        });
29650        const selectedVideos = selected ?this.cabanaLotVideos(selected) : [];
29651        const selectedCoverUrl = selected ?this.cabanaLotSelectedCoverUrl(selected) : '';
29652        const selectedItemImages = selected ?this.cabanaLotImages(selected) : [];
29653        const selectedImages = [
29654          ...(selectedCoverUrl ?[{ url: selectedCoverUrl, fileName: 'Portada del RP' }] : []),
29655          ...selectedItemImages.filter((image) => image.url !== selectedCoverUrl),
29656        ];
29657        const selectedMediaItems = [
29658          ...selectedVideos.map((video, index) => ({ ...video, type: 'video', url: video.url, thumb: video.thumb || selectedCoverUrl || '', label: 'Video ' + (index + 1), icon: 'Video' })),
29659          ...selectedImages.map((image, index) => ({ type: 'image', url: image.url, thumb: image.url, label: image.fileName || ('Imagen ' + (index + 1)), icon: 'Foto' })),
29660        ];
29661        const selectedMediaIndex = Math.min(Number(s.cbLotVideo) || 0, Math.max(selectedMediaItems.length - 1, 0));
29662        const selectedMedia = selectedMediaItems[selectedMediaIndex] || null;
29663        const selectedVideo = selectedMedia?.type === 'video' ?selectedMedia : null;
29664        const selectedImage = selectedMedia?.type === 'image' ?selectedMedia.url : selectedCoverUrl;
29665        const selectedMediaLoading = !!selected && (s.cabanaLotVideosLoading || selected.__videosLoaded !== true);
29666        const selectedMediaFailed = !!s.cabanaLotMediaFailed;
29667        const selectedHasVideo = !selectedMediaLoading && !!selectedVideo?.url;
29668        const selectedHasImage = !selectedMediaLoading && !selectedMediaFailed && !selectedVideo?.url && !!selectedImage;
29669        const selectedAssetLoading = !selectedMediaFailed && (selectedHasVideo || selectedHasImage) && !s.cabanaLotMediaReady;
29670        const dataCard = (label, keys, suffix = '') => {
29671          const rawValue = lotText(selected, keys, '');
29672          return { label, value: String(rawValue) + (!isEmptyValue(rawValue) && suffix ?suffix : ''), show: !isEmptyValue(rawValue) };
29673        };
29674        const fieldValue = (field, form) => {
29675          const installmentText=this.cabanaInstallmentField(selected,field);if(installmentText!==null)return installmentText;
29676          const names = [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name), cleanLabel(field?.name).toLowerCase().replace(/\s+/g, '_')].filter(
29676Boolean);
29677          const formKeys = [form?.key, form?.formKey, form?.name, this.consKey(form?.key), this.consKey(form?.name)].filter(Boolean);
29678          const buckets = [selected?.values, selected];
29679          formKeys.forEach((formKey) => {
29680            const bucket = selected?.subformValues?.[formKey] || selected?.subformValues?.[this.consKey(formKey)];
29681            if (bucket) buckets.unshift(bucket);
29682          });
29683          for (const bucket of buckets) {
29684            if (!bucket || typeof bucket !== 'object') continue;
29685            for (const name of names) {
29686              if (bucket[name] !== undefined && bucket[name] !== null && bucket[name] !== '') return bucket[name];
29687              const hit = Object.keys(bucket).find((key) => this.consKey(key) === this.consKey(name));
29688              if (hit && bucket[hit] !== undefined && bucket[hit] !== null && bucket[hit] !== '') return bucket[hit];
29689            }
29690          }
29691          return '';
29692        };
29693        const selectedCategorySchema = selected ?(Array.isArray(current?.formsByCategory) ?current.formsByCategory : []).find((category) => {
29694          const selectedCategoryKey = selected?.categoryId || selected?.categoryName || selected?.categoriaNombre || selected?.nombre_categoria || selected?.categoria || lotCategory(selected);
29695          return this.consKey(category?.categoryId || category?.categoryName) === this.consKey(selectedCategoryKey);
29696        }) : null;
29697        const schemaForms = selectedCategorySchema ?[
29698          ...((selectedCategorySchema.fields || []).length ?[{ name: 'Datos principales', displayOrder: 0, isActive: true, fields: selectedCategorySchema.fields }] : []),
29699          ...(selectedCategorySchema.forms || []),
29700          ...(selectedCategorySchema.subforms || []),
29701        ] : [];
29702        const storedSubforms = Object.entries(selected?.subformValues || {}).map(([key, values], index) => ({
29703          name: cleanLabel(key) || 'Datos del lote',
29704          displayOrder: index + 1,
29705          isActive: true,
29706          fields: Object.keys(values || {}).map((fieldKey, fieldIndex) => ({ key: fieldKey, name: cleanLabel(fieldKey), order: fieldIndex + 1 })),
29707        }));
29708        const uniqueForms = (forms) => {
29709          const byKey = new Map();
29710          (forms || []).forEach((form, index) => {
29711            const key = this.consKey(form?.key || form?.formKey || form?.name) || ('form' + index);
29712            const previous = byKey.get(key);
29713            if (!previous) {
29714              byKey.set(key, form);
29715              return;
29716            }
29717            const previousFieldCount = (previous.fields || []).length;
29718            const nextFieldCount = (form.fields || []).length;
29719            if (nextFieldCount > previousFieldCount) {
29720              byKey.set(key, {
29721                ...form,
29722                displayOrder: Math.min(Number(previous.displayOrder || form.displayOrder || index + 1), Number(form.displayOrder || previous.displayOrder || index + 1)),
29723              });
29724            }
29725          });
29726          return Array.from(byKey.values());
29727        };
29728        const dynamicForms = uniqueForms(schemaForms.length ?schemaForms : storedSubforms);
29729        const mainFormKey = (form) => this.consKey(form?.key || form?.formKey || form?.name).replace(/-/g, '_');
29730        const isMainForm = (form) => ['datos_principales', 'datosprincipales', 'principal', 'datos_del_lote', 'datoslote'].includes(mainFormKey(form));
29731        const mainValueSource = selected?.values || {};
29732        const explicitMainForms = dynamicForms.filter(isMainForm);
29733        const hasStructuredForms = !!(selectedCategorySchema && ((selectedCategorySchema.forms || []).length || (selectedCategorySchema.subforms || []).length));
29734        const mainSchemaFields = [
29735          ...(!hasStructuredForms ?(selectedCategorySchema?.fields || []) : []),
29736          ...explicitMainForms.flatMap((form) => form?.fields || []),
29737        ];
29738        const usedDetailKeys = new Set();
29739        const readMainValue = (field) => {
29740          const names = [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name), cleanLabel(field?.name).toLowerCase().replace(/\s+/g, '_')].filter(
29740Boolean);
29741          for (const name of names) {
29742            if (mainValueSource[name] !== undefined && mainValueSource[name] !== null && mainValueSource[name] !== '') return mainValueSource[name];
29743            const hit = Object.keys(mainValueSource).find((key) => this.consKey(key) === this.consKey(name));
29744            if (hit && mainValueSource[hit] !== undefined && mainValueSource[hit] !== null && mainValueSource[hit] !== '') return mainValueSource[hit];
29745          }
29746          return '';
29747        };
29748        const mainCardsFromSchema = mainSchemaFields.sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).filter((field) => {
29749          const key = this.consKey(field?.key || field?.name);
29750          if (!key || usedDetailKeys.has(key)) return false;
29751          usedDetailKeys.add(key);
29752          return true;
29753        }).map((field) => {
29754          const value = displayValue(readMainValue(field));
29755          [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name)].filter(Boolean).forEach((key) => usedDetailKeys.add(this.consKey(key)));
29756          return { label: String(field?.name || 'Campo').toUpperCase(), value, show: !isEmptyValue(value) };
29757        });
29758        const mainCardsFromValues = mainSchemaFields.length ?[] : Object.entries(mainValueSource).filter(([key]) => !usedDetailKeys.has(this.consKey(key))).map(([key, value]) => {
29759          usedDetailKeys.add(this.consKey(key));
29760          return { label: cleanLabel(key).toUpperCase(), value: displayValue(value), show: !isEmptyValue(displayValue(value)) };
29761        });
29762        const mainCards = [...mainCardsFromSchema, ...mainCardsFromValues].filter((card) => card.show);
29763        const formIdentityKey = (form) => this.consKey(form?.key || form?.formKey || form?.name);
29764        const isIdentityForm = (form) => ['identificacion', 'identification'].includes(formIdentityKey(form));
29765        const isDepForm = (form) => {
29766          const key = formIdentityKey(form);
29767          return key === 'deps' || key === 'dep' || key === 'depes' || key.includes('deps');
29768        };
29769        const isPrecisionForm = (form) => {
29770          const key = formIdentityKey(form);
29771          return key === 'precision' || key === 'precisiones' || key.includes('precision');
29772        };
29773        const depPrecisionForms = dynamicForms
29774          .filter((form) => form?.isActive !== false && !isMainForm(form) && (isDepForm(form) || isPrecisionForm(form)))
29775          .sort((a, b) => {
29776            const formOrder = (isDepForm(a) ?0 : 1) - (isDepForm(b) ?0 : 1);
29777            return formOrder || Number(a.displayOrder || 0) - Number(b.displayOrder || 0);
29778          });
29779        const depPrecisionKeys = new Set(depPrecisionForms.map((form) => formIdentityKey(form)));
29780        const depPrecisionColumns = [];
29781        depPrecisionForms.forEach((form) => {
29782          (form?.fields || []).sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).forEach((field) => {
29783            const key = this.consKey(field?.key || field?.name);
29784            if (!key || depPrecisionColumns.some((column) => column.key === key)) return;
29785            depPrecisionColumns.push({ key, label: String(field?.name || cleanLabel(key)).toUpperCase() });
29786          });
29787        });
29788        const detailTables = depPrecisionForms.length && depPrecisionColumns.length ?[{
29789          title: "DEP'S Y PRECISIÓN",
29790          columns: depPrecisionColumns,
29791          rows: depPrecisionForms.map((form) => ({
29792            label: isDepForm(form) ?"DEP'S" : 'PRECISIÓN',
29793            mobileLabel: isDepForm(form) ?"DEP'S" : 'PREC.',
29794            cells: depPrecisionColumns.map((column) => {
29795              const field = (form?.fields || []).find((candidate) => this.consKey(candidate?.key || candidate?.name) === column.key) || { key: column.key, name: column.label };
29796              const value = displayValue(fieldValue(field, form));
29797              [field?.key, field?.name, column.key].filter(Boolean).forEach((key) => usedDetailKeys.add(this.consKey(key)));
29798              return { label: column.label, value: isEmptyValue(value) ?'—' : value, show: !isEmptyValue(value) };
29799            }),
29800          })).filter((row) => row.cells.some((cell) => cell.show)),
29801        }].filter((table) => table.rows.length > 0) : [];
29802        const detailGroups = selected ?[
29803          ...(mainCards.length ?[{ label: 'DATOS PRINCIPALES', cards: mainCards }] : []),
29804          ...dynamicForms.filter((form) => form?.isActive !== false && !isMainForm(form) && !isIdentityForm(form) && !depPrecisionKeys.has(formIdentityKey(form))).sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)).map((form) => ({
29805            label: String(form?.name || 'DATOS DEL LOTE').toUpperCase(),
29806            cards: (form?.fields || []).sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map((field) => {
29807              const value = displayValue(fieldValue(field, form));
29808              [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name)].filter(
29808Boolean).forEach((key) => usedDetailKeys.add(this.consKey(key)));
29809              return { label: String(field?.name || 'Campo').toUpperCase(), value, show: !isEmptyValue(value) };
29810            }),
29811          })),
29812        ].map((group) => ({ ...group, cards: group.cards.filter((card) => card.show) })).filter((group) => group.cards.length > 0) : [];
29813        const extraCards = selected ?Object.entries(selected?.values || {}).filter(([key, value]) => !usedDetailKeys.has(this.consKey(key)) && !['rp', 'categoriaNombre', 'categoryName'].some((known) => this.consKey(known) === this.consKey(key))).map(([key, value]) => ({ label: cleanLabel(key).toUpperCase(), value: displayValue(value), show: !isEmptyValue(displayValue(value)) })).filter((card) => card.show) : [];
29814        if (extraCards.length) detailGroups.push({ label: 'OTROS DATOS', cards: extraCards });
29815        const isObservationCard = (card) => ['observaciones', 'observacion', 'observations', 'descripcion', 'lotdescription'].includes(this.consKey(card.label));
29816        const observationCards = detailGroups.flatMap((group) => group.cards).filter(isObservationCard);
29817        const observations = selected ?String(lotText(selected, ['observaciones', 'descripcion', 'lotDescription'], '') || observationCards.map((card) => card.value).filter(Boolean).join('\n\n')) : '';
29818        const withoutObservations = (groups) => groups.map((group) => ({ ...group, cards: group.cards.filter((card) => !isObservationCard(card)) })).filter((group) => group.cards.length);
29819        const primaryDetailGroups = withoutObservations(mainCards.length ?[{ label: 'DATOS PRINCIPALES', cards: mainCards }] : []);
29820        const secondaryDetailGroups = withoutObservations(detailGroups.filter((group) => this.consKey(group.label) !== 'datosprincipales'));
29821        const selectedCabanaForDetail = selected ?(participants.find((cabana) => (selected?.cabanaKey && cabana.key === selected.cabanaKey) || (selected?.cabanaId && String(cabana.id || '') === String(selected.cabanaId)) || (selected?.cabanaNombre && cabana.name === selected.cabanaNombre)) || {}) : {};
29822        const selectedCabanaLogo = selected ?window.resolveCabanaMediaUrl(selected.cabanaLogoDataUrl || selectedCabanaForDetail.logoDataUrl) : '';
29823        const selectedCabanaName = selected ?(selected.cabanaNombre || selected.cabanaName || selectedCabanaForDetail.name || '') : '';
29824        const detailStacked = s.isMobile || (typeof window !== 'undefined' && window.innerWidth < 1120);
29825        const cabanasInitialLoading = !!s.cabanasLoading && (!Array.isArray(s.cabanas) || !s.cabanasCollectionReady);
29826        const cabanaLotsInitialLoading = !!s.cabanaLotsLoading && !Array.isArray(s.cabanaLots);
29827        const cabanaCountsReady = Array.isArray(s.cabanaLots) && !cabanaLotsInitialLoading;
29828        const salesOrderLots = Array.isArray(s.cabanaSalesOrderLots) ?s.cabanaSalesOrderLots : [];
29829        const makeSalesOrderRow = (lot, index, scopeIndex = index, scopeCount = salesOrderLots.length) => {
29830          const cert = this.cabanaLotId(lot);
29831          const values = lot?.values || {};
29832          const identifier = values.lote || lot?.lotNumber || values.rp || lot?.rp || cert;
29833          const rp = values.rp || lot?.rp || '';
29834          return {
29835            cert,
29836            title: 'Lote ' + identifier + (rp && String(rp) !== String(identifier) ?' · RP ' + rp : ''),
29837            category: this.cabanaSalesCategoryName(lot),
29838            cabana: lot?.cabanaNombre || lot?.cabanaName || 'Cabaña sin asignar',
29839            position: index + 1,
29840            groupPosition: scopeIndex + 1,
29841            positions: Array.from({ length: scopeCount }, (_, position) => ({ value: position + 1, label: position + 1 })),
29842            isFirst: scopeIndex === 0,
29843            isLast: scopeIndex === scopeCount - 1,
29844            moveUp: () => this.setCabanaSalesLotPosition(cert, scopeIndex),
29845            moveDown: () => this.setCabanaSalesLotPosition(cert, scopeIndex + 2),
29846            setPosition: (event) => this.setCabanaSalesLotPosition(cert, event.target.value),
29847          };
29848        };
29849        const salesOrderRows = salesOrderLots.map(makeSalesOrderRow);
29850        const salesOrderGroupsRaw = [];
29851        const salesOrderGroupMap = new Map();
29852        salesOrderLots.forEach((lot, index) => {
29853          const key = this.cabanaSalesCategoryKey(lot);
29854          if (!salesOrderGroupMap.has(key)) {
29855            const group = { key, title: this.cabanaSalesCategoryName(lot), lots: [] };
29856            salesOrderGroupMap.set(key, group);
29857            salesOrderGroupsRaw.push(group);
29858          }
29859          salesOrderGroupMap.get(key).lots.push({ lot, index });
29860        });
29861        const salesOrderGroups = salesOrderGroupsRaw.map((group, groupIndex) => ({
29862          ...group,
29863          count: group.lots.length,
29864          lots: group.lots.map((entry, scopeIndex) => makeSalesOrderRow(entry.lot, entry.index, scopeIndex, group.lots.length)),
29865          position: groupIndex + 1,
29866          positions: Array.from({ length: salesOrderGroupsRaw.length }, (_, position) => ({ value: position + 1, label: position + 1 })),
29867          isFirst: groupIndex === 0,
29868          isLast: groupIndex === salesOrderGroupsRaw.length - 1,
29869          moveUp: () => this.setCabanaSalesCategoryPosition(group.key, groupIndex),
29870          moveDown: () => this.setCabanaSalesCategoryPosition(group.key, groupIndex + 2),
29871          setPosition: (event) => this.setCabanaSalesCategoryPosition(group.key, event.target.value),
29872        }));
29873        const salesOrderAccent = current ?this.cabanaColor(current) : '#0a5ca8';
29874        const auctionTitle = current?.nombre_remate || current?.title || current?.titulo || 'La Cassina';
29875        const absolutePublicUrl = (path) => {
29876          try { return new URL(path, window.location.origin).href; }
29877          catch (_) { return String(path || ''); }
29878        };
29879        const auctionPublicUrl = this.state.publicAdvanceMode === 'lacassina' ?absolutePublicUrl('/lacassina') : absolutePublicUrl(window.location.pathname || '/');
29880        const selectedPublicUrl = selected && this.state.publicAdvanceMode === 'lacassina' ?absolutePublicUrl(this.cabanaPublicLotPath(selected)) : auctionPublicUrl;
29881        const selectedRpRaw = String(selectedMapped?.rp || '').trim();
29882        const selectedRpCode = selectedRpRaw.replace(/^rp\s*/i, '').trim();
29883        const selectedRpText = selectedRpCode ?'el RP ' + selectedRpCode : 'el RP seleccionado';
29884        const contactGreeting = currentContact.name ?String(currentContact.name).split(/\s+/)[0] : '';
29885        const generalWhatsappText = 'Hola' + (contactGreeting ?' ' + contactGreeting : '') + ', quisiera consultar por los lotes del remate ' + auctionTitle + '. Enlace del remate: ' + auctionPublicUrl;
29886        const lotWhatsappText = 'Hola' + (contactGreeting ?' ' + contactGreeting : '') + ', quisiera consultar por ' + selectedRpText + ' del remate ' + auctionTitle + '. Enlace del RP: ' + selectedPublicUrl;
29887        const analyticsMetric = (row, keys) => {
29888          for (const key of keys) {
29889            const value = Number(row?.[key] || 0);
29890            if (value) return value;
29891          }
29892          return 0;
29893        };
29894        const selectedLotIdKey = this.consKey(this.cabanaLotId(selected));
29895        const selectedRpIdKey = this.consKey(this.cabanaAnalyticsRpId(selected));
29896        const analyticsRowMatchesLot = (row) => {
29897          const keys = [row?.lotId, row?.lotCert, row?.cert, row?.id].filter(Boolean).map((value) => this.consKey(value));
29898          return selectedLotIdKey && keys.includes(selectedLotIdKey);
29899        };
29900        const analyticsRowMatchesRp = (row) => {
29901          const keys = [row?.rp, row?.rpId, row?.itemId, row?.itemKey].filter(Boolean).map((value) => this.consKey(value));
29902          return selectedRpIdKey && keys.includes(selectedRpIdKey);
29903        };
29904        const selectedAnalyticsRpRow = selected ?(analyticsRps.find((row) => analyticsRowMatchesRp(row) && (!row?.lotId || analyticsRowMatchesLot(row))) || analyticsRps.find(analyticsRowMatchesRp)) : null;
29905        const selectedAnalyticsLotRow = selected ?analyticsLots.find(analyticsRowMatchesLot) : null;
29906        const selectedSocialKey = selected ?this.cabanaSocialKey(selected) : '';
29907        const selectedSocialBump = (s.cabanaSocialBumps || {})[selectedSocialKey] || {};
29908        const selectedShareCount = (analyticsMetric(selectedAnalyticsRpRow, ['shares','shareClicks','sharedCount','event_share_click']) || analyticsMetric(selectedAnalyticsLotRow, ['shares','shareClicks','sharedCount','event_share_click'])) + Number(selectedSocialBump.shares || 0);
29909        const commercialDrafts = s.cabanaCommercialDrafts || {};
29910        const commercialStatusMeta = {
29911          vendido: { label: 'Vendido', className: 'is-vendido', icon: 'check-circle-2', color: '#17704d', bg: '#e7f5ee' },
29912          pendiente: { label: 'Pendiente', className: 'is-pendiente', icon: 'clock-3', color: '#9a6700', bg: '#fff6df' },
29913          'no-vendido': { label: 'No vendido', className: 'is-no-vendido', icon: 'x-circle', color: '#c03434', bg: '#fff0f0' },
29914          levantado: { label: 'Levantado', className: 'is-levantado', icon: 'circle-off', color: '#4e6279', bg: '#eef2f6' },
29915        };
29916        const commercialStatusKeys = ['vendido', 'pendiente', 'no-vendido', 'levantado'];
29917        const commercialDefaultText = (value, fallback = '') => {
29918          const text = String(value ?? '').trim();
29919          return text ?text : fallback;
29920        };
29921        const commercialIdentifierCount = (value) => String(value ?? '')
29922          .split(/\s*(?:-|,|;|\||\r?\n)\s*/)
29923          .map((identifier) => identifier.trim())
29924          .filter(Boolean)
29925          .length;
29926        const commercialPriceDigits = (value) => String(value ?? '').replace(/\D/g, '');
29927        const commercialPriceInputFormat = new Intl.NumberFormat('es-AR', { maximumFractionDigits: 0 });
29928        const commercialNumberText = (value) => {
29929          const digits = commercialPriceDigits(value);
29930          if (!digits) return '';
29931          const parsed = Number(digits);
29932          return Number.isFinite(parsed) ?commercialPriceInputFormat.format(parsed) : digits;
29933        };
29934        const commercialParsePrice = (value) => {
29935          const digits = commercialPriceDigits(value);
29936          if (!digits) return null;
29937          const parsed = Number(digits);
29938          return Number.isFinite(parsed) && parsed > 0 ?parsed : null;
29939        };
29940        const commercialCabanaForLot = (lot) => participants.find((cabana) =>
29941          (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
29942          (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) ||
29943          (lot?.cabanaNombre && (cabana.name === lot.cabanaNombre || cabana.nombre === lot.cabanaNombre)) ||
29944          (lot?.cabanaName && (cabana.name === lot.cabanaName || cabana.nombre === lot.cabanaName))
29945        ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {};
29946        const commercialAllRows = lots.map((lot, index) => {
29947          const key = this.cabanaCommercialRowKey(lot) || String(index);
29948          const draft = commercialDrafts[key] || {};
29949          const status = commercialStatusKeys.includes(draft.status) ?draft.status : 'pendiente';
29950          const installments=window.CabanaInstallments.resolve(current?.installmentConfig,lot);
29951          const priceMode = installments.enabled?'cabeza':draft.priceMode === 'kilo' ?'kilo' : 'cabeza';
29952          const faenaEnabled = !!draft.faenaEnabled;
29953          const commercialIdentity = window.CabanaLotIdentity.resolve(lot, current);
29954          const lotNumber = commercialIdentity.number || this.cabanaLotNumber(lot) || lotText(lot, ['lote', 'lotNumber', 'numero_lote'], '');
29955          const rpRaw = String(lotText(lot, ['rp', 'RP', 'numero_rp', 'identifier'], index + 1)).replace(/^RP\s+/i, '');
29956          const paintNumber = lotText(lot, ['n', 'N', 'nro', 'numero', 'numero_pintura', 'numeroPintura', 'nro_pintura', 'pintura', 'paint', 'paintNumber'], '');
29957          const explicitQuantity = commercialDefaultText(draft.cantidad, commercialDefaultText(lotText(lot, ['cantidad', 'cant', 'cantidad_animales'], ''), ''));
29958          const corralQuantity = Math.max(Number(lot?.groupItemCount) || 0, commercialIdentifierCount(rpRaw));
29959          const quantity = explicitQuantity || (commercialIdentity.type === 'corral' && corralQuantity ?String(corralQuantity) : '');
29960          const weight = commercialDefaultText(draft.pesoProm, commercialDefaultText(lotText(lot, ['pesoPromedio', 'peso_promedio', 'peso', 'peso_actual'], ''), ''));
29961          const faenaPercent = commercialDefaultText(draft.faenaPercent, commercialDefaultText(lotText(lot, ['faena', 'porcentaje_faena', 'porcentajeFaena'], ''), ''));
29962          const loadedPrice = priceMode === 'kilo' ?commercialParsePrice(draft.precioKilo) : commercialParsePrice(draft.precioCabeza);
29963          const quantityNumeric = Number(commercialPriceDigits(quantity)) || (installments.enabled?1:0);
29964          const divisions=draft.divisions||[];
29965          const saleParts=window.RosganSaleDivisions.sales({cantidad:quantityNumeric},draft,priceMode).map(p=>({...p,price:window.CabanaInstallments.total(p.price,installments)}));
29966          const totalNumeric = saleParts.reduce((n,p)=>n+p.price*p.quantity*(priceMode==='kilo'?Number(weight)||0:1),0)||null;
29967          const commercialCabana = commercialCabanaForLot(lot);
29968          return {
29969            raw: lot,
29970            key,
29971            order: commercialDefaultText(lot?.groupDisplayOrder || lot?.displayOrder, String(index + 1)),
29972            groupLabel: commercialIdentity.label || 'Lote',
29973            groupType: commercialIdentity.type || 'lote',
29974            lotNumber: commercialDefaultText(lotNumber, '-'),
29975            rp: commercialDefaultText(rpRaw, '-'),
29976            paintNumber: commercialDefaultText(paintNumber, '-'),
29977            category: lotCategory(lot),
29978            cabana: commercialDefaultText(lot?.cabanaNombre || lot?.cabanaName || lot?.cabana?.nombre || lot?.cabana?.name || commercialCabana.name || commercialCabana.nombre, '-'),
29979            consignataria: currentConsignatariaName || 'Rosgan',
29980            categoryDisplayOrder: this.cabanaCategoryDisplayOrder(lot, index + 1),
29981            hasConsLogo: !!currentConsignatariaWhiteLogo,
29982            noConsLogo: !currentConsignatariaWhiteLogo,
29983            consLogo: currentConsignatariaWhiteLogo,
29984            consInitial: (currentConsignatariaName || 'C').charAt(0).toUpperCase(),
29985            consColor: currentConsignatariaColor,
29986            status,
29987            statusLabel: status==='pendiente'&&saleParts.length?'Venta parcial':commercialStatusMeta[status]?.label || 'Pendiente',
29988            divisions,saleParts,hasDivisions:divisions.length>0,
29989            divideSale:()=>this.openCabanaSaleDivisions(lot,quantity,lotNumber),
29990            divisionLabel:divisions.length?divisions.length+' partes · '+saleParts.reduce((n,p)=>n+p.quantity,0)+' / '+quantity+' vendidas':'Dividir venta',
29991            statusBg: commercialStatusMeta[status]?.bg || '#fff6df',
29992            statusBorder: commercialStatusMeta[status]?.color || '#9a6700',
29993            statusColor: commercialStatusMeta[status]?.color || '#9a6700',
29994            statusOptions: commercialStatusKeys.map((keyName) => ({
29995              key: keyName,
29996              label: commercialStatusMeta[keyName].label,
29997              className: keyName === status ?commercialStatusMeta[keyName].className : '',
29998              set: () => { if(divisions.length&&['vendido','pendiente'].includes(keyName)){this.openCabanaSaleDivisions(lot,quantity,lotNumber);return;}this.updateCabanaCommercialRow(lot, { status: keyName }); },
29999            })),
30000            priceMode,
30001            installmentCount:installments.count,
30002            headPriceLabel:installments.enabled?'$ / Cuota':'$ / Cabeza',
30003            installmentLabel:installments.enabled?installments.count+' cuotas'+(installments.inherited?' · General':' · Este lote'):'Cuotas del lote',
30004            installmentTotal:installments.enabled&&loadedPrice?'Total por cabeza: '+this.fmtARS(window.CabanaInstallments.total(loadedPrice,installments)):'',
30005            editInstallments:()=>this.editCabanaInstallments(lot),
30006            kiloModeClass: priceMode === 'kilo' ?'is-active' : '',
30007            cabezaModeClass: priceMode === 'cabeza' ?'is-active' : '',
30008            priceDisabled: status !== 'pendiente'||divisions.length>0,
30009            setKilo: () => { if(installments.enabled){this.showToast('La modalidad por cuota se calcula por cabeza.');return;} if (status === 'pendiente'&&!divisions.length) this.updateCabanaCommercialRow(lot, { priceMode: 'kilo' }); },
30010            setCabeza: () => { if (status === 'pendiente'&&!divisions.length) this.updateCabanaCommercialRow(lot, { priceMode: 'cabeza' }); },
30011            pricePlaceholder: installments.enabled?'$ / Cuota':priceMode === 'kilo' ?'$ / Kilo' : '$ / Cabeza',
30012            priceValue: divisions.length?'Ver divisiones':priceMode === 'kilo' ?commercialNumberText(draft.precioKilo) : commercialNumberText(draft.precioCabeza),
30013            priceNumeric: loadedPrice,
30014            priceUnit: priceMode === 'kilo' ?'$/kg' : '$/cab.',
30015            priceUnitLabel: installments.enabled?'Cuota × '+installments.count:priceMode === 'kilo' ?'Por kilo' : 'Por cabeza',
30016            priceDisplay: loadedPrice ?'$ ' + commercialPriceInputFormat.format(loadedPrice) : '-',
30017            totalNumeric,
30018            totalDisplay: totalNumeric ?'$ ' + commercialPriceInputFormat.format(totalNumeric) : '-',
30019            commercialUpdatedAt: draft.updatedAt || '',
30020            setPrice: (event) => {
30021              if (status !== 'pendiente') return;
30022              this.formatMoneyInputEvent(event, (raw) => this.queueCabanaCommercialPriceInput(lot, priceMode, raw));
30023            },
30024            commitPrice: (event) => {
30025              if (status !== 'pendiente') return;
30026              this.flushCabanaCommercialPriceInput(lot, priceMode, commercialPriceDigits(event.target.value));
30027            },
30028            commitPriceEnter: (event) => {
30029              if (event.key !== 'Enter') return;
30030              event.preventDefault();
30031              const price = commercialParsePrice(event.target.value);
30032              this.flushCabanaCommercialPriceInput(lot, priceMode, commercialPriceDigits(event.target.value), { status: price ?'vendido' : 'no-vendido' });
30033              setTimeout(() => {
30034                try {
30035                  const inputs = Array.from(document.querySelectorAll('.commercial-board .commercial-price input:not([disabled])'));
30036                  const index = inputs.indexOf(event.target);
30037                  const next = inputs[index + 1];
30038                  if (next) { next.focus(); next.select?.(); }
30039                  else event.target.blur();
30040                } catch {}
30041              }, 40);
30042            },
30043            faenaEnabled,
30044            faenaSwitchClass: faenaEnabled ?'commercial-switch is-on' : 'commercial-switch',
30045            toggleFaena: () => this.updateCabanaCommercialRow(lot, { faenaEnabled: !faenaEnabled }),
30046            faenaPercent,
30047            setFaenaPercent: (event) => this.updateCabanaCommercialRow(lot, { faenaPercent: event.target.value, faenaEnabled: true }),
30048            cantidad: quantity,
30049            setCantidad: (event) => this.updateCabanaCommercialRow(lot, { cantidad: event.target.value }),
30050            pesoProm: weight,
30051            setPesoProm: (event) => this.updateCabanaCommercialRow(lot, { pesoProm: event.target.value }),
30052            plazo: commercialDefaultText(draft.plazo, ''),
30053            setPlazo: (event) => this.updateCabanaCommercialRow(lot, { plazo: event.target.value }),
30054            comprador: commercialDefaultText(draft.comprador, ''),
30055            setComprador: (event) => this.updateCabanaCommercialRow(lot, { comprador: event.target.value }),
30056            moreOpen: s.cabanaCommercialOpenRow === key,
30057            moreClassName: s.cabanaCommercialOpenRow === key ?'commercial-more is-open' : 'commercial-more',
30058            moreIcon: 'assets/icons/lucide/' + (s.cabanaCommercialOpenRow === key ?'chevron-up' : 'chevron-down') + '.svg',
30059            toggleMore: () => this.toggleCabanaCommercialMore(lot),
30060          };
30061        });
30062        const commercialStats = commercialStatusKeys.reduce((acc, key) => ({ ...acc, [key]: commercialAllRows.filter((row) => row.status === key).length }), {});
30063        const commercialTotal = commercialAllRows.length;
30064        const commercialPct = (value) => commercialTotal ?Math.round((value / commercialTotal) * 100) + '%' : '0%';
30065        const commercialFilter = s.cabanaCommercialFilter || 'todos';
30066        const commercialFilteredRows = commercialFilter === 'todos' ?commercialAllRows : commercialAllRows.filter((row) => row.status === commercialFilter);
30067        let commercialLastCategory = '';
30068        const commercialRows = commercialFilteredRows.map((row) => {
30069          const categoryKey = this.consKey(row.category || 'Sin categoria');
30070          const showCategoryDivider = categoryKey !== commercialLastCategory;
30071          commercialLastCategory = categoryKey;
30072          return { ...row, showCategoryDivider, categoryDividerTitle: row.category || 'Sin categoria' };
30073        });
30074        const commercialReportRows = commercialFilteredRows.flatMap(row=>row.divisions?.length?row.divisions.map((part,i)=>{
30075          const sold=part.price>0&&!['no-vendido','levantado'].includes(row.status),total=sold?part.price*row.installmentCount*part.quantity*(row.priceMode==='kilo'?Number(row.pesoProm)||0:1):0;
30076          return {...row,rp:row.rp+' · Parte '+(i+1),cantidad:part.quantity,comprador:part.buyer||'',status:sold?'vendido':row.status==='vendido'?'pendiente':row.status,statusLabel:sold?'Vendido':row.status==='no-vendido'?'No vendido':row.status==='levantado'?'Levantado':'Pendiente',priceNumeric:sold?part.price:0,priceDisplay:sold?'$ '+commercialPriceInputFormat.format(part.price):'-',totalNumeric:total,totalDisplay:total?'$ '+commercialPriceInputFormat.format(total):'-'};
30077        }):[row]);
30078        const commercialView = s.cabanaCommercialView === 'lista' ?'lista' : 'carga';
30079        const commercialMetricCards = [
30080          { label: 'Total RPs', value: commercialTotal, pct: '', icon: 'list-checks', color: '#0a5ca8', bg: '#e8f2fc' },
30081          { label: 'Vendidos', value: commercialStats.vendido || 0, pct: commercialPct(commercialStats.vendido || 0), icon: 'circle-check-big', color: '#17704d', bg: '#e7f5ee' },
30082          { label: 'Pendientes', value: commercialStats.pendiente || 0, pct: commercialPct(commercialStats.pendiente || 0), icon: 'clock-3', color: '#9a6700', bg: '#fff6df' },
30083          { label: 'No vendidos', value: commercialStats['no-vendido'] || 0, pct: commercialPct(commercialStats['no-vendido'] || 0), icon: 'circle-x', color: '#c03434', bg: '#fff0f0' },
30084        ].map((item) => ({ ...item, iconUrl: 'assets/icons/lucide/' + item.icon + '.svg' }));
30085        const commercialFilters = [
30086          ['todos', 'Todos', commercialTotal],
30087          ['pendiente', 'Pendientes', commercialStats.pendiente || 0],
30088          ['vendido', 'Vendidos', commercialStats.vendido || 0],
30089          ['no-vendido', 'No vendidos', commercialStats['no-vendido'] || 0],
30090          ['levantado', 'Levantados', commercialStats.levantado || 0],
30091        ].map(([key, label, count]) => {
30092          const meta = key === 'todos'
30093            ?{ color: '#0a5ca8', bg: '#e8f2fc', activeBg: '#002e5f', activeColor: '#fff', border: '#cfe0f5' }
30094            : commercialStatusMeta[key] || commercialStatusMeta.pendiente;
30095          const active = key === commercialFilter;
30096          return {
30097            key, label, count,
30098            className: active ?'commercial-filter is-active' : 'commercial-filter',
30099            bg: active ?(meta.activeBg || meta.color) : (meta.bg || '#fff'),
30100            color: active ?(meta.activeColor || '#fff') : (meta.color || '#31526f'),
30101            border: active ?(meta.activeBg || meta.color) : (meta.color || meta.border || '#d8e5f1'),
30102            pillBg: active ?'rgba(255,255,255,.2)' : (meta.bg || 'rgba(10,92,168,.1)'),
30103            set: () => this.setCabanaCommercialFilter(key),
30104          };
30105        });
30106        const priceBoardAnimalCountCategories = new Set([
30107          'vaquillonascgtiadeprenezanguspc',
30108          'vaquillonasccriabrangus',
30109          'vaquillonascgtiadeprenezangusmas',
30110        ]);
30111        const priceLoadedGroups = new Map();
30112        commercialAllRows.forEach((row) => {
30113          if (!row.saleParts.length) return;
30114          const key = this.consKey(row.category || 'Sin categoria');
30115          if (!priceLoadedGroups.has(key)) {
30116            priceLoadedGroups.set(key, {
30117              category: row.category || 'Sin categoria',
30118              categoryDisplayOrder: row.categoryDisplayOrder || 9999,
30119              countByAnimals: priceBoardAnimalCountCategories.has(key),
30120              entries: [],
30121            });
30122          }
30123          const group = priceLoadedGroups.get(key);
30124          group.categoryDisplayOrder = Math.min(group.categoryDisplayOrder || 9999, row.categoryDisplayOrder || 9999);
30125          row.saleParts.forEach(part=>group.entries.push({value:part.price,quantity:part.quantity,updatedAt:row.commercialUpdatedAt||''}));
30126        });
30127        const priceBoardRows = Array.from(priceLoadedGroups.values()).map((group) => {
30128          const values = group.entries.map((entry) => entry.value);
30129          const count = group.countByAnimals
30130            ?group.entries.reduce((sum, entry) => sum + entry.quantity, 0)
30131            : values.length;
30132          const max = Math.max(...values);
30133          const min = Math.min(...values);
30134          const avg = group.entries.reduce((sum,entry)=>sum+entry.value*entry.quantity,0)/group.entries.reduce((sum,entry)=>sum+entry.quantity,0);
30135          const orderedEntries = [...group.entries].sort((a, b) => String(a.updatedAt || '').localeCompare(String(b.updatedAt || '')));
30136          let runningSum = 0, runningQuantity = 0;
30137          const trendHistory = orderedEntries.map((entry, index) => {
30138            runningSum += entry.value*entry.quantity;runningQuantity+=entry.quantity;
30139            return { time: entry.updatedAt || String(index), value: runningSum / runningQuantity };
30140          });
30141          return {
30142            category: group.category,
30143            categoryDisplayOrder: group.categoryDisplayOrder || 9999,
30144            count,
30145            maxValue: max,
30146            avgValue: avg,
30147            trendHistory,
30148            countRoll: this.rollInteger(count),
30149            maxRoll: this.rollNumber(max),
30150            minRoll: this.rollNumber(min),
30151            avgRoll: this.rollNumber(avg),
30152          };
30153        }).sort((a, b) => a.categoryDisplayOrder - b.categoryDisplayOrder || a.category.localeCompare(b.category));
30154        const priceTrendColors = ['#0a5ca8', '#22b4bd', '#d91549', '#17704d', '#9a6700', '#6d5dfc'];
30155        this._cabanaPriceTrendPulse = this._cabanaPriceTrendPulse || {};
30156        priceBoardRows.forEach((row) => {
30157          const key = this.consKey(row.category);
30158          const signature = (row.trendHistory || []).map((point) => Math.round(point.value)).join('|');
30159          if ((this._cabanaPriceTrendSignatures || {})[key] !== signature) {
30160            this._cabanaPriceTrendSignatures = { ...(this._cabanaPriceTrendSignatures || {}), [key]: signature };
30161            this._cabanaPriceTrendPulse[key] = (Number(this._cabanaPriceTrendPulse[key] || 0) + 1) % 2;
30162          }
30163        });
30164        const priceBoardRowsWithTrend = priceBoardRows.map((row, index) => {
30165          const history = row.trendHistory || [];
30166          const visibleHistory = history.length > 1 ?history : [history[0], history[0]].filter(Boolean);
30167          const trendValues = visibleHistory.map((point) => point.value).filter((value) => value > 0);
30168          const trendMin = trendValues.length ?Math.min(...trendValues) : 0;
30169          const trendMax = trendValues.length ?Math.max(...trendValues) : 1;
30170          const trendRange = Math.max(1, trendMax - trendMin);
30171          const trendY = (value) => trendMax === trendMin ?35 : 58 - ((value - trendMin) / trendRange) * 42;
30172          const points = visibleHistory.map((point, pointIndex, arr) => {
30173            const x = arr.length <= 1 ?0 : 12 + (pointIndex / (arr.length - 1)) * 236;
30174            const y = trendY(point.value);
30175            return Math.round(x) + ',' + Math.round(y);
30176          }).join(' ');
30177          const lastRaw = visibleHistory.slice(-1)[0] || { value: 0 };
30178          const lastX = visibleHistory.length > 1 ?248 : 12;
30179          const lastY = trendY(lastRaw.value);
30180          return { ...row, trendColor: priceTrendColors[index % priceTrendColors.length], trendClass: 'price-category-spark ' + ((this._cabanaPriceTrendPulse[this.consKey(row.category)] || 0) ?'is-a' : 'is-b'), trendPoints: points, trendLastX: Math.round(lastX), trendLastY: Math.round(lastY) };
30181        });
30182        const priceBoardUpdatedAt = Object.values(commercialDrafts || {}).map((row) =>
30182 row?.updatedAt).filter(Boolean).sort().pop();
30183        const cabanaPriceModel={title:current?.nombre_remate||current?.titulo||current?.title||'Remate de cabaña',
30184          date:(current?this.cabanaAuctionStartAt(current):null)?.toLocaleDateString('es-AR',{day:'numeric',month:'long',year:'numeric'})||'',
30185          location:current?.location?.lugar||'',logo:'/assets/rosgan-logo-blue.png',favicon:'/favicon.png',background:'/assets/lacassina-story-background.png',
30186          hideRosganLogo:true,
30187          topBrands:participants.filter(b=>b.type==='principal').map(b=>({logo:b.logoDataUrl||'',name:b.nombre||b.name||'Cabaña',plain:true})),
30188          brands:[{name:currentConsignatariaName,logo:currentConsignatariaWhiteLogo||this.consWhiteLogo(currentConsignatariaName),color:this.consBrandColor(currentConsignatariaName)}],mode:'category',
30189          // Keep roll-number components and their host references out of the serializable board model.
30190          groups:priceBoardRowsWithTrend.filter(r=>r.count>0).map(r=>({category:r.category,count:r.count,maxValue:r.maxValue,avgValue:r.avgValue,section:'Reproductores',unit:'$ / cabeza'}))};
30191        const priceBoardUpdatedLabel = priceBoardUpdatedAt ?('Actualizado ' + new Date(priceBoardUpdatedAt).toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })) : 'Esperando precios cargados';
30192        const tvGroups = allLotGroups;
30193        const tvGroupUnitLabel = (group) => window.CabanaLotIdentity.resolve(group?.variants?.[0], current).type === 'corral' ?'Corral' : 'Lote';
30194        const tvGroupLabel = (group) => group ?(tvGroupUnitLabel(group) + ' ' + group.lotNumber) : '';
30195        const storedTvKey = s.cabanaTvLotKey && tvGroups.some((group) => group.key === s.cabanaTvLotKey) ?s.cabanaTvLotKey : (tvGroups[0]?.key || '');
30196        const tvSelectedGroup = tvGroups.find((group) => group.key === storedTvKey) || tvGroups[0] || null;
30197        const tvSelectedCabana = tvSelectedGroup?.variants?.length
30198          ?commercialCabanaForLot(tvSelectedGroup.variants[0])
30199          :(participants.find((cabana) => cabana.type === 'principal') || participants[0] || {});
30200        const tvSelectedCabanaColor = /^#[0-9a-f]{6}$/i.test(String(tvSelectedCabana?.brandColor || '')) ?tvSelectedCabana.brandColor : (current ?this.cabanaColor(current) : '#0a5ca8');
30201        const tvSelectedCabanaWhiteLogo = String(tvSelectedCabana?.logoWhiteDataUrl || '');
30202        const tvSelectedCabanaColorLogo = String(tvSelectedCabana?.logoDataUrl || '');
30203        const tvSelectedCabanaLogo = [tvSelectedCabanaWhiteLogo,tvSelectedCabanaColorLogo].find(url=>url&&!s.cabanaTvLogoFailed?.[url]) || '';
30204        const tvSelectedLogoIsWhite = !!tvSelectedCabanaLogo && tvSelectedCabanaLogo === tvSelectedCabanaWhiteLogo;
30205        const tvSelectedCabanaName = String(tvSelectedCabana?.name || tvSelectedCabana?.nombre || current?.nombre_remate || current?.titulo || current?.title || 'Cabaña');
30206        const tvValueFromLot = (lot, keys, fallback = '-') => {
30207          const value = lot ?commercialDefaultText(lotText(lot, keys, ''), '') : '';
30208          return value || fallback;
30209        };
30210        const tvFieldWidth = (label) => {
30211          const key = this.consKey(label);
30212          if (/^(ce|c escrotal|circunferencia)/.test(key)) return '.5fr';
30213          if (/^(pa|peso)/.test(key)) return '.62fr';
30214          if (/rp/.test(key)) return '.66fr';
30215          if (/padre|madre|abuelo|indice/.test(key)) return '1.08fr';
30216          return '.82fr';
30217        };
30218        const tvSchema = this.cabanaRemateSchema(current);
30219        const tvCategorySchemaByKey = new Map();
30220        tvSchema.forEach((category) => {
30221          const keys = [category?.categoryId, category?.categoryName, category?.categoria, category?.name].map((key) => this.consKey(key)).filter(Boolean);
30222          keys.forEach((key) => tvCategorySchemaByKey.set(key, category));
30223        });
30224        const tvFormKey = (form, fallback = 'main') => this.consKey(form?.key || form?.formKey || form?.templateId || form?.id || form?.name || fallback);
30225        const isTvDepForm = (form) => {
30226          const key = tvFormKey(form);
30227          return key === 'deps' || key === 'dep' || key === 'depes' || key.includes('deps');
30228        };
30229        const isTvPrecisionForm = (form) => {
30230          const key = tvFormKey(form);
30231          return key === 'precision' || key === 'precisiones' || key.includes('precision');
30232        };
30233        const buildTvForms = (categoryName) => {
30234          const categoryKey = this.consKey(categoryName || '');
30235          const tvSchemaCategory = tvCategorySchemaByKey.get(categoryKey);
30236          return tvSchemaCategory ?[
30237            ...((tvSchemaCategory.fields || []).length ?[{ name: 'Datos principales', displayOrder: 0, isActive: true, fields: tvSchemaCategory.fields }] : []),
30238            ...(tvSchemaCategory.forms || []),
30239            ...(tvSchemaCategory.subforms || []),
30240          ].filter((form) => form?.isActive !== false).sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)) : [];
30241        };
30242        const buildTvFieldGroups = (categoryName) => {
30243          const seen = new Set(['rp']);
30244          const groups = [{ key: 'identity', label: 'Identificacion', fields: [{ key: 'rp', label: 'RP', fr: '.66fr', value: (variant) => tvValueFromLot(variant, ['rp', 'RP', 'numero_rp'], this.cabanaAnalyticsRpId(variant) || '-') }] }];
30245          const forms = buildTvForms(categoryName).filter((form) => !isTvDepForm(form) && !isTvPrecisionForm(form));
30246          forms.forEach((form) => {
30247            const fields = [];
30248            (form.fields || [])
30249              .slice()
30250              .sort((a, b) => Number(a?.order || a?.displayOrder || 0) - Number(b?.order || b?.displayOrder || 0))
30251              .forEach((field, fieldIndex) => {
30252                const label = String(field?.name || field?.nombre || field?.label || field?.key || '').trim();
30253                if (!label) return;
30254                const key = 'field:' + tvFormKey(form, 'main') + ':' + this.consKey(field?.key || field?.name || field?.nombre || label || fieldIndex);
30255                if (seen.has(key) || key === 'field::') return;
30256                seen.add(key);
30257                fields.push({
30258                  key,
30259                  label: this.cabanaStoryCleanLabel(label),
30260                  fr: tvFieldWidth(label),
30261                  value: (variant) => {
30262                    const value = this.cabanaStoryDisplayValue(this.cabanaLotFieldValue(variant, field, form));
30263                    return value || '-';
30264                  },
30265                });
30266              });
30267            if (fields.length) groups.push({ key: tvFormKey(form, groups.length), label: form?.name || form?.nombre || 'Subformulario', fields });
30268          });
30269          return groups;
30270        };
30271        const buildTvFieldDefs = (categoryName) => {
30272          const defs = buildTvFieldGroups(categoryName).flatMap((group) => group.fields || []);
30273          if (defs.length === 1) {
30274            [
30275              { key: 'ce', label: 'CE', fr: '.5fr', value: (variant) => tvValueFromLot(variant, ['ce', 'circunferencia_escrotal', 'c_escrotal'], '-') },
30276              { key: 'pa', label: 'PA', fr: '.62fr', value: (variant) => tvValueFromLot(variant, ['pesoProm', 'peso_promedio', 'pesoPromedio', 'peso', 'peso_actual'], '-') },
30277              { key: 'padre', label: 'Padre', fr: '1.08fr', value: (variant) => tvValueFromLot(variant, ['padre', 'father'], '-') },
30278            ].forEach((field) => defs.push(field));
30279          }
30280          return defs;
30281        };
30282        const tvSelectedCategoryKey = this.consKey(tvSelectedGroup?.categoryName || '');
30283        const tvFieldDefsByCategory = {};
30284        const tvFieldGroupsByCategory = {};
30285        const tvCategoryOptions = [];
30286        tvGroups.forEach((group) => {
30287          const key = this.consKey(group.categoryName || '');
30288          if (!key || tvFieldDefsByCategory[key]) return;
30289          tvFieldDefsByCategory[key] = buildTvFieldDefs(group.categoryName);
30290          tvFieldGroupsByCategory[key] = buildTvFieldGroups(group.categoryName);
30291          tvCategoryOptions.push({ key, label: group.categoryName || 'Sin categoria', count: tvGroups.filter((item) => this.consKey(item.categoryName || '') === key).length });
30292        });
30293        const tvFieldDefs = tvFieldDefsByCategory[tvSelectedCategoryKey] || buildTvFieldDefs(tvSelectedGroup?.categoryName || '');
30294        const tvSelectedFieldGroups = tvFieldGroupsByCategory[tvSelectedCategoryKey] || buildTvFieldGroups(tvSelectedGroup?.categoryName || '');
30295        this._cabanaTvFieldKeysByCategory = Object.fromEntries(Object.entries(tvFieldDefsByCategory).map(([key, defs]) => [key, defs.map((field) => field.key)]));
30296        this._cabanaTvActiveCategoryKey = tvSelectedCategoryKey;
30297        this._cabanaTvFieldKeys = tvFieldDefs.map((field) => field.key);
30298        const tvConfig = this.normalizeCabanaTvConfig(s.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys, tvSelectedCategoryKey);
30299        const tvSelectedFields = this.cabanaTvFieldsForCategory(tvConfig, tvSelectedCategoryKey, this._cabanaTvFieldKeys);
30300        const tvColumns = tvFieldDefs.filter((field) => tvSelectedFields.includes(field.key));
30301        if (!tvFieldDefsByCategory[tvSelectedCategoryKey] && tvFieldDefs.length) {
30302          tvFieldDefsByCategory[tvSelectedCategoryKey] = tvFieldDefs;
30303          tvFieldGroupsByCategory[tvSelectedCategoryKey] = tvSelectedFieldGroups;
30304          this._cabanaTvFieldKeysByCategory[tvSelectedCategoryKey] = tvFieldDefs.map((field) => field.key);
30305        }
30306        const tvVariantCount = tvSelectedGroup?.variants?.length || 0;
30307        this._cabanaTvRollSigs = this._cabanaTvRollSigs || {};
30308        this._cabanaTvRollPulses = this._cabanaTvRollPulses || {};
30309        const tvRollClassFor = (id, value) => {
30310          const text = String(value ?? '');
30311          if (this._cabanaTvRollSigs[id] !== text) {
30312            this._cabanaTvRollSigs[id] = text;
30313            this._cabanaTvRollPulses[id] = (Number(this._cabanaTvRollPulses[id] || 0) + 1) % 2;
30314            return 'tv-roll ' + (this._cabanaTvRollPulses[id] ?'is-a' : 'is-b');
30315          }
30316          return 'tv-roll';
30317        };
30318        const formatTvSalePrice = (draft) => {
30319          if (!draft || typeof draft !== 'object') return '';
30320          const mode = draft.priceMode === 'kilo' ?'kilo' : 'cabeza';
30321          const raw = mode === 'kilo' ?draft.precioKilo : draft.precioCabeza;
30322          const digits = String(raw ?? '').replace(/\D/g, '');
30323          if (!digits) return '';
30324          const price = '$ ' + Number(digits).toLocaleString('es-AR');
30325          return mode === 'kilo' ?(price + ' / kg') : price;
30326        };
30327        const tvCommercialForVariant = (variant) => {
30328          const key = this.cabanaCommercialRowKey(variant);
30329          const draft = (commercialDrafts || {})[key] || this.normalizeCabanaCommercialRecord(variant?.commercial);
30330          const status = String(draft?.status || 'pendiente').trim().toLowerCase();
30331          const isSold = status === 'vendido' || status === 'sold';
30332          const isLifted = status === 'levantado' || status === 'lifted';
30333          const price = formatTvSalePrice(draft);
30334          const saleLabel = draft.divisions?.length&&!isLifted&&status!=='no-vendido' ? draft.divisions.filter(p=>p.price>0).map(p=>p.quantity+' × $ '+Number(p.price).toLocaleString('es-AR')).join(' · ') || 'A la venta' : isSold ?(price || 'Vendido') : (isLifted ?'Anulado' : 'A la venta');
30335          return { status, isSold, isLifted, price, saleLabel, hasSignal: isSold || isLifted };
30336        };
30337        const tvMainVariants = (tvSelectedGroup?.variants || []).slice(0, 10);
30338        const tvVariantQuantity = (variant) => {
30339          const draft = (commercialDrafts || {})[this.cabanaCommercialRowKey(variant)] || this.normalizeCabanaCommercialRecord(variant?.commercial);
30340          const raw = commercialDefaultText(draft?.cantidad, commercialDefaultText(lotText(variant, ['cantidad', 'cantidadTotal', 'cantidad_animales', 'quantity', 'totalQuantity'], ''), ''));
30341          const quantity = Number(String(raw ?? '').trim().replace(',', '.'));
30342          return Number.isFinite(quantity) && quantity > 0 ?quantity : 1;
30343        };
30344        const tvQuantityCount = tvMainVariants.reduce((total, variant) => total + tvVariantQuantity(variant), 0) || tvVariantCount;
30345        const tvCommercialSignals = tvMainVariants.map((variant) => tvCommercialForVariant(variant));
30346        const tvSaleBadgesBase = tvMainVariants.map((variant, rowIndex) => {
30347          const commercial = tvCommercialSignals[rowIndex] || tvCommercialForVariant(variant);
30348          if (!commercial.hasSignal) return null;
30349          const rp = tvValueFromLot(variant, ['rp', 'RP', 'numero_rp'], this.cabanaAnalyticsRpId(variant) || '-');
30350          if (commercial.isSold) {
30351            return {
30352              className: 'tv-lower-sale-badge is-sold',
30353              label: 'Vendido',
30354              text: 'RP ' + rp + (commercial.price ?(' · ' + commercial.price) : ' · Sin precio'),
30355            };
30356          }
30357          return {
30358            className: 'tv-lower-sale-badge is-lifted',
30359            label: 'Anulado',
30360            text: 'RP ' + rp,
30361          };
30362        }).filter(Boolean);
30363        const tvMainColumns = [
30364          ...tvColumns,
30365          ...(tvConfig.saleColumnVisible !== false ?[{ key: 'sale', label: 'Venta', fr: '.72fr', isSale: true, value: () => 'A la venta' }] : []),
30366        ];
30367        const tvRows = tvMainVariants.map((variant, rowIndex) => {
30368          const commercial = tvCommercialSignals[rowIndex] || tvCommercialForVariant(variant);
30369          const cells = tvMainColumns.map((column) => {
30370            const value = column.isSale ?commercial.saleLabel : column.value(variant);
30371            const saleClass = column.isSale ?('tv-lower-sale-cell ' + (commercial.isSold ?'is-sold' : (commercial.isLifted ?'is-lifted' : 'is-empty'))) : '';
30372            const length = String(value ?? '').trim().length;
30373            const lengthClass = column.isSale ?'' : (length > 90 ?' is-very-long' : (length > 42 ?' is-long' : ''));
30374            return { key: column.key, value, className: saleClass + lengthClass, rollClass: tvRollClassFor('row-' + rowIndex + '-' + column.key, value) };
30375          });
30376          return { className: 'tv-lower-row tv-lower-body ' + (commercial.isSold ?'is-sold' : (commercial.isLifted ?'is-lifted' : '')), cells };
30377        });
30378        const tvDepForms = buildTvForms(tvSelectedGroup?.categoryName || '').filter((form) => isTvDepForm(form) || isTvPrecisionForm(form));
30379        const tvDepFieldLabel = (field, fieldIndex = 0) => this.cabanaStoryCleanLabel(field?.name || field?.nombre || field?.label || field?.key || ('Dato ' + (fieldIndex + 1)));
30380        const tvDepFormType = (form) => isTvPrecisionForm(form) ?'precision' : 'deps';
30381        const tvDepFormLabel = (form) => tvDepFormType(form) === 'precision' ?'Precision' : 'DEPs';
30382        const tvDepRowsForVariant = (variant) => {
30383          const valuesByField = new Map();
30384          tvDepForms.forEach((form) => {
30385            const type = tvDepFormType(form);
30386            (form.fields || []).slice()
30387              .sort((a, b) => Number(a?.order || a?.displayOrder || 0) - Number(b?.order || b?.displayOrder || 0))
30388              .forEach((field, fieldIndex) => {
30389                const label = tvDepFieldLabel(field, fieldIndex);
30390                if (!label) return;
30391                const rawValue = this.cabanaStoryDisplayValue(this.cabanaLotFieldValue(variant, field, form));
30392                if (isEmptyValue(rawValue)) return;
30393                const key = this.consKey(field?.key || field?.name || label || fieldIndex);
30394                if (!valuesByField.has(key)) valuesByField.set(key, { key, label, deps: '', precision: '' });
30395                valuesByField.get(key)[type] = rawValue;
30396              });
30397          });
30398          return Array.from(valuesByField.values()).filter((item) => !isEmptyValue(item.deps) || !isEmptyValue(item.precision));
30399        };
30400        const tvDepCards = (tvSelectedGroup?.variants || []).slice(0, 10).map((variant, variantIndex) => {
30401          const fields = tvDepRowsForVariant(variant);
30402          return {
30403            variant,
30404            variantIndex,
30405            rp: tvValueFromLot(variant, ['rp', 'RP', 'numero_rp'], this.cabanaAnalyticsRpId(variant) || '-'),
30406            fields,
30407          };
30408        }).filter((item) => item.fields.length);
30409        const tvClock = Number(s.cabanaTvClock || Date.now());
30410        const tvCycleStartedAt = Number(s.cabanaTvCycleStartedAt || tvClock);
30411        const tvMainDurationMs = 10000;
30412        const tvRpDurationMs = 5000;
30413        const tvCycleMs = tvMainDurationMs + (tvDepCards.length * tvRpDurationMs);
30414        const tvPhaseMs = tvConfig.depsEnabled === true && tvCycleMs > tvMainDurationMs ?Math.max(0, tvClock - tvCycleStartedAt) % tvCycleMs : 0;
30415        const tvDepCardIndex = tvPhaseMs >= tvMainDurationMs ?Math.min(tvDepCards.length - 1, Math.floor((tvPhaseMs - tvMainDurationMs) / tvRpDurationMs)) : -1;
30416        const tvActiveDepCard = tvDepCardIndex >= 0 ?tvDepCards[tvDepCardIndex] : null;
30417        const tvShowingDeps = !!tvActiveDepCard;
30418        const tvGraphicsVisible = tvConfig.visible !== false;
30419        const tvAllVideoItems = this.cabanaTvVideoItems(tvSelectedGroup);
30420        const tvVideoMode = tvConfig.videoMode === 'grid' ?'grid' : 'single';
30421        const tvFullScreenPlateActive = tvGraphicsVisible && (tvConfig.waitingVisible === true || tvConfig.endVisible === true);
30422        const tvPlaybackMode = tvFullScreenPlateActive ?'single' : tvVideoMode;
30423        const tvStatusVideoItems = tvAllVideoItems.map((item) => {
30424          const commercial = tvCommercialForVariant(item.variant);
30425          return { ...item, isSold: commercial.isSold, isLifted: commercial.isLifted };
30426        });
30427        const tvModeVideoItems = tvStatusVideoItems.filter((item) => !item.isLifted && (tvPlaybackMode === 'grid' || !item.isSold));
30428        const tvVideosPending = tvModeVideoItems.some((item) => item.needsLoad);
30429        const tvPlaybackRequested = tvGraphicsVisible && (tvFullScreenPlateActive || tvConfig.videoVisible === true);
30430        if (tvVideosPending) {
30431          setTimeout(() => this.ensureCabanaTvVideos({ variants: tvModeVideoItems.map((item) => item.variant) }), 0);
30432        }
30433        const tvReadyVideoItems = tvModeVideoItems.filter((item) => item.src);
30434        const tvRequestedVideoIndex = tvReadyVideoItems.findIndex((item) => item.key === tvConfig.videoRpKey);
30435        const tvSelectedVideoIndex = tvRequestedVideoIndex >= 0 ?tvRequestedVideoIndex : 0;
30436        const tvSelectedVideoItem = tvReadyVideoItems[tvSelectedVideoIndex] || null;
30437        const tvSinglePlaylistItems = tvReadyVideoItems.length ?[
30438          ...tvReadyVideoItems.slice(tvSelectedVideoIndex),
30439          ...tvReadyVideoItems.slice(0, tvSelectedVideoIndex),
30440        ] : [];
30441        const tvVideoItems = tvPlaybackMode === 'grid' ?tvReadyVideoItems.slice(0, 4) : tvSinglePlaylistItems;
30442        this._cabanaTvReadyVideoCount = tvReadyVideoItems.length;
30443        if (s.route === 'cabanaTvLower' && tvVideoItems.length) {
30444          setTimeout(() => this.syncCabanaTvVideoMount(tvVideoItems, tvPlaybackMode, {
30445            playing: tvPlaybackRequested && (tvFullScreenPlateActive || tvConfig.videoPlaying !== false),
30446            prewarming: !tvPlaybackRequested,
30447            restartToken: tvConfig.videoRestartToken,
30448            lowerThirdVisible: tvConfig.lowerThirdVisible !== false,
30449            waitingBackground: tvFullScreenPlateActive,
30450          }), 0);
30451        } else if (this._cabanaTvVideoMountSignature && s.route === 'cabanaTvLower' && !tvVideosPending) {
30452          setTimeout(() => this.destroyCabanaTvVideoMount(), 0);
30453        }
30454        const tvDepColumns = tvActiveDepCard ?[
30455          { key: 'label', label: '', fr: '.98fr' },
30456          ...tvActiveDepCard.fields.map((field) => ({ key: field.key, label: field.label, fr: tvFieldWidth(field.label) })),
30457        ] : [];
30458        const tvDepRows = tvActiveDepCard ?[
30459          { key: 'field-names', cells: tvDepColumns.map((column, columnIndex) => ({ key: 'name-' + column.key, value: columnIndex === 0 ?'Campo' : column.label, className: '', rollClass: tvRollClassFor('dep-' + tvActiveDepCard.variantIndex + '-name-' + column.key, columnIndex === 0 ?'Campo' : column.label) })) },
30460          { key: 'deps-values', cells: tvDepColumns.map((column, columnIndex) => {
30461            const field = columnIndex === 0 ?null : tvActiveDepCard.fields[columnIndex - 1];
30462            const value = columnIndex === 0 ?'DEPs' : (field?.deps || '-');
30463            return { key: 'deps-' + column.key, value, className: '', rollClass: tvRollClassFor('dep-' + tvActiveDepCard.variantIndex + '-deps-' + column.key, value) };
30464          }) },
30465          { key: 'precision-values', cells: tvDepColumns.map((column, columnIndex) => {
30466            const field = columnIndex === 0 ?null : tvActiveDepCard.fields[columnIndex - 1];
30467            const value = columnIndex === 0 ?'Precision' : (field?.precision || '-');
30468            return { key: 'precision-' + column.key, value, className: '', rollClass: tvRollClassFor('dep-' + tvActiveDepCard.variantIndex + '-precision-' + column.key, value) };
30469          }) },
30470        ].map((row) => ({ ...row, className: row.className || 'tv-lower-row tv-lower-body' })) : [];
30471        const tvActiveColumns = tvShowingDeps ?tvDepColumns : tvMainColumns;
30472        const tvActiveRows = tvShowingDeps ?tvDepRows : tvRows;
30473        const tvTextUnits = (value) => Array.from(String(value ?? '')).reduce((total, char) => {
30474          if (/\s/.test(char)) return total + .38;
30475          if (/[.,:;!|/\\()\-]/.test(char)) return total + .48;
30476          if (/\d/.test(char)) return total + .74;
30477          if (char === char.toUpperCase() && char !== char.toLowerCase()) return total + 1.06;
30478          return total + .9;
30479        }, 0);
30480        const tvMaxColumnWeight = tvActiveColumns.length <= 5 ?2 : (tvActiveColumns.length === 6 ?1.8 : (tvActiveColumns.length === 7 ?1.6 : 1.4));
30481        const tvGridCols = tvActiveColumns.map((column, columnIndex) => {
30482          const configuredWeight = Math.max(.48, Number.parseFloat(column?.fr) || .82);
30483          const contentUnits = [column?.label, ...tvActiveRows.map((row) => row?.cells?.[columnIndex]?.value)]
30484            .reduce((longest, value) => Math.max(longest, tvTextUnits(value)), 0);
30485          let contentWeight = .38 + (contentUnits * .055);
30486          if (column?.isSale) contentWeight = Math.min(contentWeight, 1.02);
30487          const weight = Math.min(tvMaxColumnWeight, Math.max(configuredWeight, contentWeight));
30488          return (Math.round(weight * 100) / 100) + 'fr';
30489        }).join(' ') || '1fr';
30490        const tvGroupsCount = tvGroups.length;
30491        const tvIndex = Math.max(0, tvGroups.findIndex((group) => group.key === storedTvKey));
30492        const setTvByIndex = (index) => {
30493          if (!tvGroupsCount) return;
30494          const next = tvGroups[((index % tvGroupsCount) + tvGroupsCount) % tvGroupsCount];
30495          if (next) this.setCabanaTvLotKey(next.key);
30496        };
30497        const tvParams = new URLSearchParams(window.location.search || '');
30498        const tvClean = tvParams.get('clean') === '1';
30499        const tvWeather = s.cabanaTvWeather || null;
30500        const tvWeatherIcons = new Set(['sun', 'cloud', 'cloud-sun', 'cloud-rain', 'cloud-drizzle', 'cloud-lightning', 'cloud-snow', 'cloud-fog', 'cloudy', 'wind']);
30501        const tvWeatherIconName = tvWeatherIcons.has(String(tvWeather?.icon || '')) ?String(tvWeather.icon) : 'cloud';
30502        const tvWeatherIcon = tvWeather ?('assets/icons/lucide/' + tvWeatherIconName + '.svg') : '';
30503        const tvWeatherUnit = String(tvWeather?.unit || '°C').replace(/^Â/, '');
30504        const tvWaitingStart = this.cabanaAuctionStartAt(current);
30505        const tvWaitingRemainingSeconds = tvWaitingStart ?Math.max(0, Math.ceil((tvWaitingStart.getTime() - tvClock) / 1000)) : 0;
30506        const tvWaitingCountdownActive = !!tvWaitingStart && tvWaitingRemainingSeconds > 0;
30507        const tvWaitingDays = Math.floor(tvWaitingRemainingSeconds / 86400);
30508        const tvWaitingHours = Math.floor((tvWaitingRemainingSeconds % 86400) / 3600);
30509        const tvWaitingMinutes = Math.floor((tvWaitingRemainingSeconds % 3600) / 60);
30510        const tvWaitingSeconds = tvWaitingRemainingSeconds % 60;
30511        const tvWaitingPad = (value) => String(Math.max(0, Number(value) || 0)).padStart(2, '0');
30512        const tvWaitingLocation = current?.ubicacion_remate || current?.lugar || [current?.location?.lugar, current?.localidad || current?.location?.localidad, current?.provincia || current?.location?.provinciaNombre].filter(Boolean).filter((value, index, list) => list.indexOf(value) === index).join(', ') || 'Localidad a confirmar';
30513        return {
30514          cabanasLoading: cabanasInitialLoading, cabanaSkeletonCards: Array.from({ length: 6 }, (_, index) => ({ index })), cabanasError: s.cabanasError, cabanasHasError: !!s.cabanasError && (!s.cabanasCollectionReady || cabanaCards.length === 0),
30515          hasCabanas: !cabanasInitialLoading && cabanaCards.length > 0, cabanasEmpty: !s.cabanasLoading && !!s.cabanasCollectionReady && Array.isArray(s.cabanas) && !s.cabanasError && cabanaCards.length === 0,
30516          cabanaBreedChips: [{ label: 'Todas', key: '', count: cabanas.length }, ...breeds.map((breed) => ({ label: breed, key: breed, count: cabanas.filter((item) => this.cabanaBreedOf(item) === breed).length }))].map((chip) => ({ ...chip, go: () => this.setState({ cabanaBreed: chip.key }), bg: s.cabanaBreed === chip.key ?'#002e5f' : '#fff', color: s.cabanaBreed === chip.key ?'#fff' : '#3d4c5f' })),
30517          cabanaCards,
30518          cbColor: currentHeroColor, cbBrandDark: current ?this.cabanaBrandDark(current) : '#002e5f', cbMedia: current ?this.cabanaMedia(current) : '', cbHasMedia: !!(current && this.cabanaMedia(current)),
30519          cbHeroText: currentHeroIsDark ?'#fff' : '#102033', cbHeroMuted: currentHeroIsDark ?'rgba(255,255,255,.7)' : 'rgba(16,32,51,.68)', cbHeroSurface: currentHeroIsDark ?'rgba(0,24,55,.2)' : 'rgba(255,255,255,.38)', cbHeroSurfaceStrong: currentHeroIsDark ?'rgba(255,255,255,.12)' : 'rgba(255,255,255,.58)', cbHeroBorder: currentHeroIsDark ?'rgba(255,255,255,.2)' : 'rgba(16,32,51,.18)', cbHeroIconFilter: currentHeroIsDark ?'brightness(0) invert(1)' : 'none',
30520          cbBrandPanelClass: mainBrandLogos.length > 4 ?'is-compact' : '', cbManyMainBrands: mainBrandLogos.length > 4, cbMainBrandCount: mainBrandLogos.length, cbFewMainBrands: mainBrandLogos.length <= 4, cbNameRollDuration: Math.max(32, mainBrandLogos.length * 5) + 's', cbNameRollPaused: !!s.cbNameRollPaused, cbNameRollPausedClass: s.cbNameRollPaused ?'is-paused':'', cbNameRollPauseLabel: s.cbNameRollPaused ?'Reanudar':'Pausar', cbToggleNameRoll: () => this.setState({cbNameRollPaused:!this.state.cbNameRollPaused}), cbMainLogos: mainBrandLogos, cbGuestLogos: guestBrandLogos,
30521          cbManyGuestBrands: guestBrandLogos.length > 4, cbFewGuestBrands: guestBrandLogos.length > 0 && guestBrandLogos.length <= 4, cbGuestBrandCount: guestBrandLogos.length,
30522          cbGuestRollDuration: Math.max(32, guestBrandLogos.length * 5) + 's', cbGuestRollPaused: !!s.cbGuestRollPaused, cbGuestRollPausedClass: s.cbGuestRollPaused ?'is-paused':'', cbGuestRollPauseLabel: s.cbGuestRollPaused ?'Reanudar':'Pausar', cbToggleGuestRoll: () => this.setState({cbGuestRollPaused:!this.state.cbGuestRollPaused}), cbHasBranding: mainBrandLogos.length > 0 || guestBrandLogos.length > 0, cbHasGuestLogos: guestBrandLogos.length > 0, cbPrimaryWhiteLogo: primaryWhiteLogo, cbHasPrimaryWhiteLogo: !!primaryWhiteLogo, cbHeroGrid: s.isMobile ?'1fr' : 'minmax(0,1.15fr) minmax(320px,.85fr)', cbMainLogoHeight: s.isMobile ?'62px' : '82px',
30523          cbTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña', cbBreed: current ?(this.cabanaBreedOf(current) || 'Reproductores') : '', cbDate: current ?dateLabel(current) : '', cbTime: current ?timeLabel(current) : '',
30524          cbLocation: current ?(current.ubicacion_remate || current.localidad || current.provincia || current?.location?.lugar || current?.location?.localidad || current?.location?.provinciaNombre || 'Ubicación a confirmar') : '', cbConsignataria: currentConsignatariaName,
30525          cbConsignatariaLogo: currentConsignatariaLogo, cbHasConsignatariaLogo: !!currentConsignatariaLogo, cbNoConsignatariaLogo: !currentConsignatariaLogo, cbConsignatariaInitial: (currentConsignatariaName || 'R').charAt(0).toUpperCase(),
30526          cbConsignatariaColor: currentConsignatariaColor, cbConsignatariaShadow: currentConsignatariaColor + '35', cbConsignatariaWhiteLogo: currentConsignatariaWhiteLogo, cbHasConsignatariaWhiteLogo: !!currentConsignatariaWhiteLogo, cbNoConsignatariaWhiteLogo: !currentConsignatariaWhiteLogo,
30527          cbContactName: currentContact.name, cbContactPhone: currentContact.phone, cbContactPhoneHref: currentContact.phoneHref,
30528          cbContactWhatsappHref: currentContact.whatsappBase + (currentContact.phoneDigits ?'?text=' + encodeURIComponent(generalWhatsappText) : ''),
30529          cbLoading: cabanaLotsInitialLoading, cbCountsLoading: !cabanaCountsReady, cbCountsReady: cabanaCountsReady,
30530          cbHasLots: lotCards.length > 0, cbLotsEmpty: !s.cabanaLotsLoading && Array.isArray(s.cabanaLots) && !s.cabanaLotsError && lotCards.length === 0,
30531          cbGroupUnit: groupUnit, cbGroupUnitUpper: groupUnit.toUpperCase(), cbCatalogTitle: groupUnit === 'corrales' ? 'Corrales disponibles' : groupUnit === 'lotes' ? 'Lotes disponibles' : 'Lotes y corrales disponibles',
30532          cbTotal: totalLotGroups, cbLots: lotCards,
30533          cbHasCatalogPdf: !!this.cabanaCatalogPdfUrl(current), cbCatalogPdfUrl: this.cabanaCatalogPdfUrl(current),
30534          cbCatalogLocked: this.cabanaCatalogLocked(current), cbCatalogUnlocked: !this.cabanaCatalogLocked(current), cbCatalogLogin: () => this.loginForCabanaCatalog(),
30535          cbQuery: s.cbQuery, onCbQuery: (event) => this.setState({ cbQuery: event.target.value }),
30536          cbPrebidSummaryAllowed: !!s.user?.uid && s.cabanaPrebidSummaryAccess?.uid === s.user.uid && s.cabanaPrebidSummaryAccess?.id === this.cabanaRemateId(current),
30537          cbOpenPrebidSummary: () => this.openCabanaPrebidSummary(),
30538          cbTermsVisible: this.canEditCabanaCommercialTerms(current) || (s.cabanaCommercialTerms?.id===this.cabanaRemateId(current) && !!s.cabanaCommercialTerms?.imageUrl),
30539          cbOpenTerms: () => this.openCabanaCommercialTerms(),
30540          cbPrebidPanelAllowed: !!s.cabanaPrebidPanelAllowed,
30541          cbPrebidPanelVisible: !!s.cabanaPrebidPanelAllowed && prebidPanelRawItems.length > 0,
30542          cbPrebidPanelOpen: !!s.cabanaPrebidPanelOpen,
30543          cbPrebidPanelLoading: !!s.cabanaPrebidPanelLoading,
30544          cbPrebidPanelHasError: !!s.cabanaPrebidPanelError,
30545          cbPrebidPanelError: s.cabanaPrebidPanelError || '',
30546          cbPrebidPanelRows: prebidPanelRows,
30547          cbPrebidPanelHasRows: prebidPanelRows.length > 0,
30548          cbPrebidPanelEmpty: !s.cabanaPrebidPanelLoading && prebidPanelRows.length === 0,
30549          cbPrebidPanelMetrics: prebidPanelMetrics,
30550          cbPrebidPanelLots: prebidPanelLots,
30551          cbPrebidPanelHasLots: prebidPanelLots.length > 0,
30552          cbPrebidPanelHasSelectedLot: !!prebidPanelSelectedLot,
30553          cbPrebidPanelSelectedLotLabel: prebidPanelSelectedLot?.label || '',
30554          cbPrebidPanelUpdated: prebidPanelUpdated,
30555          cbPrebidPanelQuery: s.cabanaPrebidPanelQuery || '',
30556          cbPrebidPanelStatus: s.cabanaPrebidPanelStatus || 'all',
30557          cbPrebidPanelStatusTabs: [['all','Todas'],['current','Actuales'],['pending','Pendientes'],['outbid','Superadas']].map(([key, label]) => ({ key, label, active: prebidPanelStatus === key, className: prebidPanelStatus === key ?'cb-prebid-admin-filter is-active' : 'cb-prebid-admin-filter', go: () => this.setState({ cabanaPrebidPanelStatus: key }) })),
30558          cbCanManagePrivatePages: this.canManageCabanaPrivatePages(s.apiUser),
30559          cbCanSeeStatsButton: this.canSeeCabanaStats(current, s.apiUser),
30560          cbCanAudit: this.canAuditCabana(current),
30561          cbAuditorOpen: this.canAuditCabana(current) && s.route === 'cabanaAuditor',
30562          toggleCbAuditor: () => this.openCabanaAuditor(current),
30563          closeCbAuditor: () => this.openCabana(current),
30564          cbAuditorName: current?.name || current?.nombre || current?.titulo || '',
30565          cbAuditorCount: auditorRows.length,
30566          cbAuditorLoading: !!s.cabanaLotsLoading,
30567          cbAuditorError: s.cabanasError || '',
30568          refreshCbAuditor: () => { if(this.canAuditCabana(current)) return this.fetchCabanaLots(current); },
30569          cbAuditorQuery: s.cabanaAuditorQuery || '',
30570          searchCbAuditor: event => this.setState({cabanaAuditorQuery:event.target.value}),
30571          cbAuditorRows: auditorRows,
30572          cbAuditorEmpty: !s.cabanaLotsLoading && !auditorRows.length,
30573          cbShowAuctionSubpages: this.canAuditCabana(current) || this.canSeeCabanaStats(current, s.apiUser) || this.canManageCabanaPrivatePages(s.apiUser),
30574          openCbPrebidPanel: () => this.openCabanaPrebidPanelPage(current),
30575          closeCbPrebidPanel: () => this.closeCabanaPrebidPanelPage(),
30576          toggleCbPrebidPanel: () => this.toggleCabanaPrebidPanel(),
30577          refreshCbPrebidPanel: () => this.fetchCabanaPrebidPanel(current),
30578          onCbPrebidPanelQuery: (event) => this.setState({ cabanaPrebidPanelQuery: event.target.value }),
30579          cbCanManageSalesOrder: !!current && this.canManageUsers(),
30580          cbCanExportCatalog: this.canExportCabanaCatalog(current, s.apiUser),
30581          openCbSalesOrder: () => this.openCabanaSalesOrder(),
30582          openCbStats: () => this.openCabanaStats(current),
30583          openCbCommercial: () => this.openCabanaCommercial(current),
30584          openCbPrices: () => this.openCabanaPriceBoard(current),
30585          openCbTv: () => this.openCabanaTvLower(current),
30586          openCbTvConfig: () => this.openCabanaTvConfig(current),
30587          downloadCbCatalogExcel: () => this.downloadCabanaCatalogExcel(),
30588          ...this.cabanaStatsData(current, s.cabanaLots),
30589          cbSalesOrderAuctionTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña',
30590          cbSalesOrderOpen: !!s.cabanaSalesOrderOpen,
30591          cbSalesOrderGrouped: !!s.cabanaSalesOrderGrouped,
30592          cbSalesOrderGroupLabel: s.cabanaSalesOrderGrouped ?'Vista por orden general' : 'Agrupar por categoría',
30593          cbSalesOrderSaved: !!s.cabanaSalesOrderSaved,
30594          cbSalesOrderFlat: !s.cabanaSalesOrderGrouped,
30595          cbSalesOrderRows: salesOrderRows,
30596          cbSalesOrderGroups: salesOrderGroups,
30597          cbSalesOrderCount: salesOrderLots.length,
30598          cbSalesOrderLoading: !!s.cabanaLotsLoading,
30599          cbSalesOrderEmpty: !s.cabanaLotsLoading && salesOrderLots.length === 0,
30600          cbSalesOrderHasError: !!s.cabanaSalesOrderError,
30601          cbSalesOrderError: s.cabanaSalesOrderError || '',
30602          cbSalesOrderSaving: !!s.cabanaSalesOrderSaving,
30603          cbSalesOrderSaveOpacity: s.cabanaSalesOrderSaving ?'.62' : '1',
30604          cbSalesOrderSaveLabel: s.cabanaSalesOrderSaving ?'Guardando...' : 'Guardar orden',
30605          cbSalesGroupBg: s.cabanaSalesOrderGrouped ?salesOrderAccent : '#fff',
30606          cbSalesGroupColor: s.cabanaSalesOrderGrouped ?'#fff' : '#3d4c5f',
30607          cbSalesGroupBorder: s.cabanaSalesOrderGrouped ?salesOrderAccent : '#cbd8e7',
30608          closeCbSalesOrder: () => this.closeCabanaSalesOrder(),
30609          toggleCbSalesOrderGrouped: () => this.toggleCabanaSalesOrderGrouped(),
30610          saveCbSalesOrder: () => this.saveCabanaSalesOrder(),
30611          cbInstallmentSummary:current?.installmentConfig?.enabled?'Se preoferta una cuota · '+current.installmentConfig.count+' cuotas generales. Cada lote puede tener su propia cantidad.':'Desactivada. Los importes se consideran totales.',
30612          cbEditInstallments:()=>this.editCabanaInstallments(),
30613          cbCommercialTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabana',
30614          cbCommercialGroupLabel: groupUnit === 'corrales' ?'Corral' : (groupUnit === 'lotes / corrales' ?'Lote / Corral' : 'Lote'),
30615          cbCommercialRows: commercialRows,
30616          cbCommercialReportRows: commercialReportRows,
30617          cbCommercialMetrics: commercialMetricCards,
30618          cbCommercialFilters: commercialFilters,
30619          cbCommercialHasRows: commercialRows.length > 0,
30620          cbCommercialEmpty: !s.cabanaLotsLoading && commercialRows.length === 0,
30621          cbCommercialHasReportRows: commercialReportRows.length > 0,
30622          cbCommercialReportEmpty: !s.cabanaLotsLoading && commercialReportRows.length === 0,
30623          cbCommercialReportCountLabel: commercialReportRows.length === commercialTotal ?commercialTotal + ' registros' : commercialReportRows.length + ' de ' + commercialTotal + ' registros',
30624          cbCommercialIsEntry: commercialView === 'carga',
30625          cbCommercialIsList: commercialView === 'lista',
30626          cbCommercialEntryTabClass: commercialView === 'carga' ?'commercial-view-tab is-active' : 'commercial-view-tab',
30627          cbCommercialListTabClass: commercialView === 'lista' ?'commercial-view-tab is-active' : 'commercial-view-tab',
30628          cbCommercialShowEntry: () => this.setState({ cabanaCommercialView: 'carga' }),
30629          cbCommercialShowList: () => this.setState({ cabanaCommercialView: 'lista', cabanaCommercialOpenRow: '' }),
30630          cbCommercialExportExcel: () => this.downloadCabanaCommercialExcel(commercialReportRows),
30631          cbCommercialLoading: !!s.cabanaLotsLoading || !!s.cabanaCommercialLoading,
30632          cbCommercialOpenPrices: () => this.openCabanaPriceBoard(current),
30633          cbCommercialBack: () => this.backToCabanaCatalog(),
30634          isCabanaPriceBoard: s.route === 'cabanaPriceBoard' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/precios')),
30635          pbTitle: current?.nombre_remate || current?.titulo || current?.title || 'La Cassina',
30636          pbRows: priceBoardRowsWithTrend,
30637          pbHasRows: priceBoardRowsWithTrend.length > 0,
30638          pbEmpty: !s.cabanaLotsLoading && priceBoardRowsWithTrend.length === 0,
30639          pbUpdatedLabel: priceBoardUpdatedLabel,
30640          pbMainLogo: (mainBrandLogos[0] && mainBrandLogos[0].logo) || '',
30641          pbHasMainLogo: !!(mainBrandLogos[0] && mainBrandLogos[0].logo),
30642          pbNoMainLogo: !(mainBrandLogos[0] && mainBrandLogos[0].logo),
30643          pbConsignataria: currentConsignatariaName,
30644          pbConsLogo: currentConsignatariaWhiteLogo || currentConsignatariaLogo,
30645          pbHasConsLogo: !!(currentConsignatariaWhiteLogo || currentConsignatariaLogo),
30646          pbNoConsLogo: !(currentConsignatariaWhiteLogo || currentConsignatariaLogo),
30647          pbConsInitial: (currentConsignatariaName || 'C').charAt(0).toUpperCase(),
30648          pbConsColor: currentConsignatariaColor,
30649          pbBackCommercial: () => this.openCabanaCommercial(current),
30650          pbBoardData:JSON.stringify(cabanaPriceModel),
30651          pbExportStory: () => this.downloadCabanaPriceBoardStory(priceBoardRowsWithTrend,cabanaPriceModel),
30652          pbStoryBusy: !!s.cabanaPriceStoryBusy,
30653          pbStoryLabel: s.cabanaPriceStoryBusy ?'Generando...' : 'Exportar Story',
30654          pbOpenTv: () => this.openCabanaTvConfig(current),
30655          pbBackCatalog: () => this.backToCabanaCatalog(),
30656          isCabanaTvLower: s.route === 'cabanaTvLower' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv')),
30657          isCabanaTvConfig: s.route === 'cabanaTvConfig' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv/config')),
30658          tvShowControls: false,
30659          tvHasLot: !!tvSelectedGroup && tvGraphicsVisible,
30660          tvEmpty: !tvSelectedGroup,
30661          tvLogo: tvSelectedCabanaLogo,
30662          tvLogoError:()=>{if(tvSelectedCabanaLogo)this.setState({cabanaTvLogoFailed:{...this.state.cabanaTvLogoFailed,[tvSelectedCabanaLogo]:true}});},
30663          tvHasLogo: !!tvSelectedCabanaLogo,
30664          tvNoLogo: !tvSelectedCabanaLogo,
30665          tvRosganLogo: '/assets/rosgan-logo-blue.png',
30666          tvKicker: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabana',
30667          tvClockLabel: new Date(tvClock || Date.now()).toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }),
30668          tvLogosVisible: tvConfig.logosVisible !== false,
30669          tvClockVisible: tvConfig.clockVisible !== false,
30670          tvKickerVisible: tvConfig.kickerVisible !== false,
30671          tvChannelBugVisible: tvConfig.logosVisible !== false || tvConfig.clockVisible !== false || tvConfig.kickerVisible !== false,
30672          tvLiveBugVisible: tvConfig.clockVisible !== false || tvConfig.kickerVisible !== false,
30673          tvWeatherVisible: !!tvWeather && tvConfig.weatherVisible !== false,
30674          tvWeatherIcon,
30675          tvWeatherTemp: tvWeather ?(String(tvWeather.temperature) + tvWeatherUnit) : '',
30676          tvWeatherLabel: tvWeather?.label || '',
30677          tvWeatherLocation: tvWeather?.location || s.cabanaTvWeatherLocation || '',
30678          tvWaitingVisible: tvGraphicsVisible && tvConfig.waitingVisible === true,
30679          tvEndVisible: tvGraphicsVisible && tvConfig.endVisible === true,
30680          tvWaitingTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña',
30681          tvWaitingDate: tvWaitingStart ?tvWaitingStart.toLocaleDateString('es-AR', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }) : 'Fecha a confirmar',
30682          tvWaitingTime: tvWaitingStart ?(tvWaitingStart.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' h') : 'Hora a confirmar',
30683          tvWaitingLocation,
30684          tvWaitingCountdownLabel: tvWaitingCountdownActive ?'El remate comienza en' : (tvWaitingStart ?'Estamos listos' : 'Próximamente'),
30685          tvWaitingCountdownActive,
30686          tvWaitingMessageVisible: !tvWaitingCountdownActive,
30687          tvWaitingMessage: tvWaitingStart ?'Enseguida comienza el remate' : 'Horario a confirmar',
30688          tvWaitingCountdownParts: [
30689            { label: 'Días', value: tvWaitingPad(tvWaitingDays) },
30690            { label: 'Horas', value: tvWaitingPad(tvWaitingHours) },
30691            { label: 'Minutos', value: tvWaitingPad(tvWaitingMinutes) },
30692            { label: 'Segundos', value: tvWaitingPad(tvWaitingSeconds) },
30693          ],
30694          tvWaitingNow: new Date(tvClock || Date.now()).toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
30695          tvWaitingConsLogoFilter: currentConsignatariaWhiteLogo ?'none' : 'brightness(0) invert(1)',
30696          tvVideoVisible: tvGraphicsVisible && tvConfig.videoVisible === true && tvVideoItems.length > 0,
30697          tvLowerThirdVisible: tvConfig.lowerThirdVisible !== false,
30698          tvCabanaName: tvSelectedCabanaName,
30699          tvCabanaColor: tvSelectedCabanaColor,
30700          tvCabanaLogoFilter:'none',
30701          tvCabanaLogoBackground:tvSelectedLogoIsWhite?tvSelectedCabanaColor:'#ffffff',
30702          tvConsName: currentConsignatariaName,
30703          tvConsLogo: currentConsignatariaWhiteLogo || currentConsignatariaLogo,
30704          tvHasConsLogo: !!(currentConsignatariaWhiteLogo || currentConsignatariaLogo),
30705          tvNoConsLogo: !(currentConsignatariaWhiteLogo || currentConsignatariaLogo),
30706          tvConsInitial: (currentConsignatariaName || 'C').charAt(0).toUpperCase(),
30707          tvConsColor: currentConsignatariaColor,
30708          tvLotLabel: tvGroupLabel(tvSelectedGroup),
30709          tvActiveRpLabel: tvShowingDeps ?('RP ' + tvActiveDepCard.rp) : '',
30710          tvHasActiveRp: tvShowingDeps,
30711          tvActiveRpRollClass: tvRollClassFor('active-rp', tvShowingDeps ?('RP ' + tvActiveDepCard.rp) : ''),
30712          tvCategory: tvSelectedGroup?.categoryName || '',
30713          tvCount: String(tvQuantityCount),
30714          tvRows: tvActiveRows,
30715          tvColumns: tvActiveColumns,
30716          tvShowTableHead: !tvShowingDeps,
30717          tvGridCols,
30718          tvTableClass: 'tv-lower-table ' + (tvShowingDeps ?'is-deps is-single-dep ' : '') + (Math.max(tvActiveRows.length, tvActiveColumns.length) >= 8 ?'is-max' : (Math.max(tvActiveRows.length, tvActiveColumns.length) >= 5 ?'is-many' : '')),
30719          tvLotOptions: tvGroups.map((group) => ({ key: group.key, label: tvGroupLabel(group) + ' - ' + group.categoryName })),
30720          tvGroupListTitle: groupUnit === 'corrales' ?'Corrales' : (groupUnit === 'lotes' ?'Lotes' : 'Lotes y corrales'),
30721          tvSelectedKey: storedTvKey,
30722          tvVisibleLabel: tvConfig.visible === false ?'Gráfica oculta' : 'Gráfica visible',
30723          tvVisibleSwitchClass: tvConfig.visible === false ?'tv-config-switch' : 'tv-config-switch is-on',
30724          tvToggleVisible: () => this.toggleCabanaTvVisible(),
30725          tvContactsText: tvConfig.contacts || '',
30726          tvContactsChange: (event) => this.saveCabanaTvConfig({ contacts: event.target.value }),
30727          tvShowContacts: tvConfig.visible !== false && tvConfig.contactsVisible === true && !tvConfig.waitingVisible && !tvConfig.endVisible && !!String(tvConfig.contacts || '').trim(),
30728          tvScreenElements: [
30729            ['contactsVisible', 'Ticker de teléfonos'],
30730            ['lowerThirdVisible', 'Zócalo'],
30731            ['saleColumnVisible', 'Columna Venta'],
30732            ['waitingVisible', 'Placa de espera'],
30733            ['endVisible', 'Fin del remate'],
30734            ['logosVisible', 'Mosca de logos'],
30735            ['weatherVisible', 'Clima'],
30736            ['clockVisible', 'Hora'],
30737            ['kickerVisible', 'Nombre del remate'],
30738            ['videoVisible', 'Videos'],
30739          ].map(([key, label]) => ({ key, label, className: tvConfig[key] === true ?'tv-config-screen-toggle is-on' : 'tv-config-screen-toggle', disabled: (tvConfig.waitingVisible === true || tvConfig.endVisible === true) && !['waitingVisible', 'endVisible'].includes(key), toggle: () => this.toggleCabanaTvElement(key) })),
30740          tvVideosConfigVisible: tvConfig.videoVisible === true,
30741          tvVideoModeOptions: [
30742            ['single', '1 RP'],
30743            ['grid', '2-4 videos'],
30744          ].map(([mode, label]) => ({ mode, label, className: tvVideoMode === mode ?'is-active' : '', select: () => this.setCabanaTvVideoMode(mode) })),
30745          tvVideoTransportVisible: tvVideoMode === 'single',
30746          tvVideoTransportDisabled: tvReadyVideoItems.length === 0,
30747          tvVideoTransportRp: tvSelectedVideoItem?.rp || (tvVideosPending ?'Cargando videos...' : 'Sin videos disponibles'),
30748          tvVideoPlaybackStatus: tvReadyVideoItems.length ?(tvConfig.videoPlaying === false ?'Pausado' : 'Reproduciendo') : 'Sin señal',
30749          tvVideoPlaybackAction: tvConfig.videoPlaying === false ?'Reproducir' : 'Pausar',
30750          tvVideoSelectedKey: tvSelectedVideoItem?.key || '',
30751          tvVideoRpOptions: tvReadyVideoItems.map((item) => ({ key: item.key, label: item.rp || 'RP' })),
30752          tvSelectVideoRp: (event) => this.setCabanaTvVideoRpKey(event.target.value),
30753          tvPrevVideoRp: () => this.stepCabanaTvVideoRp(tvReadyVideoItems, -1),
30754          tvNextVideoRp: () => this.stepCabanaTvVideoRp(tvReadyVideoItems, 1),
30755          tvRestartVideo: () => this.restartCabanaTvVideo(tvSelectedVideoItem?.key || ''),
30756          tvToggleVideoPlayback: () => this.toggleCabanaTvVideoPlayback(tvSelectedVideoItem?.key || ''),
30757          tvDepsLabel: tvConfig.depsEnabled === true ?'DEPs activos' : 'DEPs ocultos',
30758          tvDepsSwitchClass: tvConfig.depsEnabled === true ?'tv-config-switch is-on' : 'tv-config-switch',
30759          tvToggleDeps: () => this.toggleCabanaTvDeps(),
30760          tvDepsModeLabel: tvShowingDeps ?('RP ' + tvActiveDepCard.rp) : '',
30761          tvShowingDeps,
30762          tvConfigBack: () => this.backToCabanaCatalog(),
30763          tvCategoryFieldGroups: tvCategoryOptions.map((category) => {
30764            const defs = tvFieldDefsByCategory[category.key] || [];
30765            const allowed = defs.map((field) => field.key);
30766            const selected = this.cabanaTvFieldsForCategory(tvConfig, category.key, allowed);
30767            return {
30768              key: category.key,
30769              label: category.label,
30770              subforms: (tvFieldGroupsByCategory[category.key] || []).map((group) => ({
30771                key: group.key,
30772                label: group.label,
30773                fields: (group.fields || []).map((field) => ({ key: field.key, label: field.label, checked: selected.includes(field.key), toggle: () => this.toggleCabanaTvField(field.key, category.key) })),
30774              })),
30775            };
30776          }),
30777          tvFieldOptions: tvFieldDefs.map((field) => ({ key: field.key, label: field.label, checked: tvSelectedFields.includes(field.key), toggle: () => this.toggleCabanaTvField(field.key, tvSelectedCategoryKey) })),
30778          tvLotRows: tvGroups.map((group) => {
30779            const variants = Array.isArray(group.variants) ?group.variants : [];
30780            const statuses = variants.map((variant) => tvCommercialForVariant(variant).status);
30781            const allLifted = statuses.length > 0 && statuses.every((status) => status === 'levantado' || status === 'lifted');
30782            const allResolved = statuses.length > 0 && statuses.every((status) => status && status !== 'pendiente' && status !== 'pending');
30783            const classes = ['tv-config-lot'];
30784            if (group.key === storedTvKey) classes.push('is-active');
30785            if (allLifted) classes.push('is-all-lifted');
30786            return {
30787              key: group.key,
30788              lotNumber: tvGroupLabel(group),
30789              category: group.categoryName,
30790              count: variants.length,
30791              className: classes.join(' '),
30792              disabled: allLifted,
30793              statusClass: 'tv-config-lot-status ' + (allLifted ?'is-lifted' : (allResolved ?'is-complete' : 'is-empty')),
30794              statusIsLifted: allLifted,
30795              statusIsResolved: !allLifted && allResolved,
30796              statusLabel: allLifted ?'Todos los RP levantados' : (allResolved ?'Todos los RP procesados' : 'Tiene RP pendientes'),
30797              select: () => { if (!allLifted) this.setCabanaTvLotKey(group.key); },
30798            };
30799          }),
30800          tvLowerClass: 'tv-lower-third',
30801          tvCategoryRollClass: tvRollClassFor('category', tvSelectedGroup?.categoryName || ''),
30802          tvCountRollClass: tvRollClassFor('count', String(tvQuantityCount)),
30803          tvLotRollClass: tvRollClassFor('lot', tvGroupLabel(tvSelectedGroup)),
30804          tvSelectLot: (event) => this.setCabanaTvLotKey(event.target.value),
30805          tvPrev: () => setTvByIndex(tvIndex - 1),
30806          tvNext: () => setTvByIndex(tvIndex + 1),
30807            goCabanas: () => this.go('cabanas'), goCabanaBack: () => { this.flushCabanaLotViewTimer('back_to_catalog'); if (this.state.publicAdvanceMode === 'lacassina') { this.writeAppHistory('/lacassina'); window.__ROSGAN_PUBLIC_PATH__ = '/lacassina'; window.__ROSGAN_PUBLIC_LOT_NUMBER__ = ''; window.__ROSGAN_PUBLIC_RP__ = ''; } else this.syncCabanaLocation(this.state.currentCabana); this.setState({ route: 'cabanaDetail', currentCabanaLot: null, currentCabanaLotVariants: [], cbLotVideo: 0, cabanaLotVideosLoading: false, cabanaLotMediaReady: false, cabanaLotMediaFailed: false, cabanaPublicDeepLoading: false }); },
30808          clHasRpVariants: clRpVariants.length > 1, clRpVariants,
30809          clTitle: selectedMapped?.hasLotNumber ?(selectedMapped.lotLabel + ' ' + selectedMapped.lotNumber) : (selectedMapped?.name || selectedMapped?.lotLabel || 'Lote'),
30810          clSubtitle: selectedMapped?.category || '', clName: selectedMapped?.name || selectedMapped?.category || '', clRp: selectedMapped?.rp || '', clCategory: selectedMapped?.category || '', clBreed: selectedMapped?.breed || '', clStatus: selectedMapped?.status || '', clStatusBg: selectedMapped?.statusBg || '#65758a', clShowStatus: false,
30811          clBase: selectedMapped?.base || 'Consultar', clMediaLoading: selectedMediaLoading || selectedAssetLoading,
30812          clMediaLoaded: (event) => {
30813            mediaReady(event);
30814            const expectedUrl = selectedVideo?.url ?new URL(selectedVideo.url, window.location.href).href : '';
30815            if (!selectedVideo?.url && selectedImage) this.trackCabanaAnalyticsOnce('image_view', selected, { mediaId: selectedImage, imageId: selectedImage, mediaType: 'image', metadata: { action: 'image_visible', mediaIndex: selectedMediaIndex } });
30816            if (!expectedUrl || event?.currentTarget?.currentSrc === expectedUrl || event?.currentTarget?.src === expectedUrl) this.setState({ cabanaLotMediaReady: true, cabanaLotMediaFailed: false });
30817          },
30818          clMediaError: (event) => {
30819            const expectedUrl = selectedVideo?.url ?new URL(selectedVideo.url, window.location.href).href : '';
30820            if (!expectedUrl || event?.currentTarget?.currentSrc === expectedUrl || event?.currentTarget?.src === expectedUrl) this.setState({ cabanaLotMediaReady: true, cabanaLotMediaFailed: true });
30821          },
30822          clVideoUrl: selectedVideo?.url || '', clVideoPoster: selectedVideo?.thumb || selectedImage || '',
30823          clVideoPlay: (event) => { this.syncCabanaVideoState(event?.currentTarget, true); this.trackCabanaVideoPlay(selected, selectedVideo?.url, selectedMediaIndex); },
30824          clVideoPause: (event) => this.syncCabanaVideoState(event?.currentTarget, true),
30825          clVideoMeta: (event) => this.syncCabanaVideoState(event?.currentTarget, true),
30826          clVideoTimeUpdate: (event) => this.syncCabanaVideoState(event?.currentTarget, false),
30827          clVideoVolumeChange: (event) => this.syncCabanaVideoState(event?.currentTarget, true),
30828          clVideoEnded: (event) => {
30829            if (selectedVideo?.url) this.trackCabanaAnalytics('video_complete', selected, { mediaId: selectedVideo.url, videoId: selectedVideo.url, mediaType: 'video', metadata: { action: 'video_ended', mediaIndex: selectedMediaIndex } });
30830            const video = event?.currentTarget;
30831            if (video) {
30832              video.currentTime = 0;
30833              video.play?.().catch(() => {});
30834              this.syncCabanaVideoState(video, true);
30835            }
30836          },
30837          clTogglePlay: (event) => this.toggleCabanaVideoPlayback(event),
30838          clSeekVideo: (event) => this.seekCabanaVideo(event),
30839          clToggleMute: (event) => this.toggleCabanaVideoMute(event),
30840          clSetVolume: (event) => this.setCabanaVideoVolume(event),
30841          clFullscreen: (event) => this.openCabanaVideoFullscreen(event),
30842          clPlayIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoPlaying ?'pause' : 'play') + '.svg',
30843          clPlayLabel: s.cabanaLotVideoPlaying ?'Pausar video' : 'Reproducir video',
30844          clVolumeIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoMuted ?'volume-x' : 'volume-2') + '.svg',
30845          clMuteLabel: s.cabanaLotVideoMuted ?'Activar sonido' : 'Silenciar video',
30846          clVideoProgress: Math.round(Number(s.cabanaLotVideoProgress || 0) * 10) / 10,
30847          clVideoVolume: Math.round(Number(s.cabanaLotVideoVolume || 0) * 100) / 100,
30848          clVideoTime: this.formatCabanaVideoTime((Number(s.cabanaLotVideoProgress || 0) / 100) * Number(s.cabanaLotVideoDuration || 0)) + ' / ' + this.formatCabanaVideoTime(s.cabanaLotVideoDuration),
30849           clHasVideo: selectedHasVideo, clImage: selectedImage, clShowImage: selectedHasImage,
30850           clEmptyMedia: !selectedMediaLoading && !selectedVideo?.url && !selectedImage,
30851           clPlaybackError: !selectedMediaLoading && selectedMediaFailed && !!(selectedVideo?.url || selectedImage),
30852           clRetryMedia: () => {
30853             this.setState({ cabanaLotMediaFailed: false, cabanaLotMediaReady: false }, () => this.syncCabanaLotVideo(this.cabanaLotId(this.state.currentCabanaLot)));
30854           },
30855           clCanUploadMedia: this.canUploadCabanaLotMedia(current, selected),
30856           clUploadMedia: (event) => this.quickUploadCabanaLotVideo(event),
30857           clUploadMediaBusy: !!s.cabanaLotQuickVideoUploading,
30858           clCanSelectUpload: this.canUploadCabanaLotMedia(current, selected) && !s.cabanaLotQuickVideoUploading,
30859           clUploadProgress: Math.max(0, Math.min(100, Number(s.cabanaLotQuickVideoProgress || 0))),
30860           clUploadStage: s.cabanaLotQuickVideoStage || 'Preparando la carga…',
30861           clThumbs: selectedMediaItems.map((media, index) => {
30862            const thumbUrl = media.type === 'video' ?media.thumb : (media.thumb || media.url);
30863            return { thumb: thumbUrl, hasThumb: !!thumbUrl, label: media.label, icon: media.icon, mediaReady, mediaError, border: index === selectedMediaIndex ?(current ?this.cabanaColor(current) : '#0a5ca8') : '#dce5ef', select: () => {
30864              const activeLotId = this.cabanaLotId(this.state.currentCabanaLot);
30865              if (media.type === 'image') this.trackCabanaAnalyticsOnce('image_view', this.state.currentCabanaLot, { mediaId: media.url, imageId: media.url, mediaType: 'image', metadata: { action: 'select_image', mediaIndex: index } });
30866              this.setState({ cbLotVideo: index, cabanaLotMediaReady: false, cabanaLotMediaFailed: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true });
30867              this.syncCabanaLotVideo(activeLotId);
30868            } };
30869          }),
30870          clHasThumbs: selectedMediaItems.length > 1, clCabana: current?.nombre_remate || current?.title || current?.titulo || '', clConsignataria: currentConsignatariaName || current?.nombre_cuenta || current?.consignataria || 'Rosgan',
30871          clConsignatariaColor: currentConsignatariaColor, clConsignatariaShadow: currentConsignatariaColor + '35', clConsignatariaWhiteLogo: currentConsignatariaWhiteLogo,
30872          clHasConsignatariaWhiteLogo: !!currentConsignatariaWhiteLogo, clNoConsignatariaWhiteLogo: !currentConsignatariaWhiteLogo, clConsignatariaInitial: (currentConsignatariaName || 'R').charAt(0).toUpperCase(),
30873          clContactName: currentContact.name, clContactPhone: currentContact.phone, clContactPhoneHref: currentContact.phoneHref,
30874          clContactWhatsappHref: currentContact.whatsappBase + (currentContact.phoneDigits ?'?text=' + encodeURIComponent(lotWhatsappText) : ''),
30875          clContactWhatsapp: () => this.trackCabanaAnalytics('contact_whatsapp_click', selected, { metadata: { action: 'contact_whatsapp', contactName: currentContact.name } }),
30876          clContactCall: () => this.trackCabanaAnalytics('contact_call_click', selected, { metadata: { action: 'contact_call', contactName: currentContact.name } }),
30877          clCanEdit: this.canEditCabanaLot(current, selected),
30878          clEditPricing: () => this.editCabanaPrebidPricing(),
30879          clEdit: () => this.openCabanaLotEdit(current, selected),
30880          clCanDelete: this.canDeleteCabanaLot(current, selected),
30881          clDelete: () => this.deleteCabanaLot(current, selected),
30882          clDeleting: !!selected && s.cabanaLotDeleting === (this.cabanaPrebidCert(selected) || this.cabanaLotId(selected)),
30883          clDeleteLabel: !!selected && s.cabanaLotDeleting === (this.cabanaPrebidCert(selected) || this.cabanaLotId(selected)) ?'Eliminando...' : 'Eliminar lote',
30884          clDeleteOpacity: s.cabanaLotDeleting ?'.6' : '1', clDeleteCursor: s.cabanaLotDeleting ?'wait' : 'pointer',
30885          clCanDeleteVideo: !!selectedVideo?.id && this.canExportCabanaCatalog(current, s.apiUser),
30886          clDeleteVideo: (event) =>
30886 this.deleteCabanaLotVideo(current, selected, selectedVideo, event),
30887          clVideoDeleting: !!selectedVideo?.id && s.cabanaLotVideoDeleting === selectedVideo.id,
30888          clDeleteVideoLabel: !!selectedVideo?.id && s.cabanaLotVideoDeleting === selectedVideo.id ?'Eliminando video…' : 'Eliminar video',
30889          clShare: () => this.shareCabanaLot(selected),
30890          clShareInstagramStory: () => this.shareCabanaLotStory(selected, { remate: current, contact: currentContact }),
30891          clShareWhatsappStatus: () => this.shareCabanaLotStory(selected, { remate: current, contact: currentContact }),
30892          clInstagramStoryLabel: s.cabanaLotStoryBusy ?'Generando...' : 'Instagram Stories',
30893          clWhatsappStatusLabel: s.cabanaLotStoryBusy ?'Generando...' : 'WhatsApp Estado',
30894          clShareCount: selectedShareCount.toLocaleString('es-AR'),
30895          clCabanaName: selectedCabanaName, clCabanaLogo: selectedCabanaLogo, clHasCabanaLogo: !!selectedCabanaLogo, clHasCabanaName: !!selectedCabanaName, clShowCabanaNameCard: !!selectedCabanaName && !selectedCabanaLogo,
30896          clTables: detailTables, clPrimaryGroups: primaryDetailGroups, clSecondaryGroups: secondaryDetailGroups, clShowBaseCard: !!(prebidActive && (selectedPrebidWinner || (selectedMapped?.base && selectedMapped.base !== 'Consultar'))),
30897          clPrebidAmountLabel:selectedInstallments.enabled?'MONTO POR CUOTA':'MONTO',
30898          clInstallmentsEnabled:selectedInstallments.enabled,
30899          clInstallmentPriceTotal:selectedInstallments.count+' cuotas · Total: '+this.fmtARS(window.CabanaInstallments.total(selectedPrebidWinner?.amount??selectedPrebidBase,selectedInstallments)),
30900          clInstallmentOfferTotal:selectedInstallments.count+' cuotas de '+this.fmtARS(this.cabanaPrebidNumber(selectedPrebidForm.amount||selectedPrebidSuggested))+' · Total por cabeza: '+this.fmtARS(window.CabanaInstallments.total(this.cabanaPrebidNumber(selectedPrebidForm.amount||selectedPrebidSuggested),selectedInstallments)),
30901          clOwnWinning:!!selectedOwnWinner, clOwnWinningLabel:selectedPrebidWindowOpen?'Estás ganando':'Ganaste la preoferta',
30902          clPriceBackground:selectedOwnWinner?'linear-gradient(145deg,#075d46,#119b70)':'linear-gradient(145deg,#002e5f,'+this.cabanaColor(current)+')',
30903          clPriceLabel: (selectedPrebidWinner ?'OFERTA ACTUAL' : 'PRECIO BASE')+(selectedInstallments.enabled?' / CUOTA':''),
30904          clPriceAmount: selectedPrebidWinner ?this.fmtARS(selectedPrebidWinner.amount) : (selectedMapped?.base || 'Consultar'),
30905          clObservations: observations, clHasObservations: !isEmptyValue(observations),
30906          clPrebidEnabled: prebidActive,
30907          clPrebidOpen: prebidWindow.active,
30908          clPrebidUpcoming: prebidWindow.upcoming,
30909          clPrebidClosed: !prebidWindow.active && !prebidWindow.upcoming,
30910          clPrebidCardClass: prebidWindow.active ? '' : 'is-inactive',
30911          clPrebidStart: prebidWindow.start || 0,
30912          clPrebidStartLabel: prebidWindow.upcoming ? new Date(prebidWindow.start).toLocaleString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',day:'numeric',month:'long',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}) + ' h (Argentina)' : '',
30913
30914          clPrebidLoading: !!s.cabanaPrebidsLoading,
30915          clPrebidHasError: !!s.cabanaPrebidsError,
30916          clPrebidError: s.cabanaPrebidsError || '',
30917          clPrebidHasWinner: !!selectedPrebidWinner,
30918          clPrebidCurrentAmount: selectedPrebidWinner ?this.fmtARS(selectedPrebidWinner.amount) : 'Sin preofertas',
30919          clPrebidCurrentRoll: selectedPrebidWinner ?this.rollNumber(selectedPrebidWinner.amount) : 'Sin preofertas',
30920          clPrebidSuggestedAmount: this.fmtARS(selectedPrebidSuggested),
30921          clPrebidIncrement: this.fmtARS(selectedPrebidIncrement),
30922          clPrebidHistory: selectedPrebidHistory,
30923          clPrebidHasHistory: selectedPrebidHistory.length > 0,
30924          clPrebidEmpty: !s.cabanaPrebidsLoading && !s.cabanaPrebidsError && selectedPrebidHistory.length === 0,
30925          clOpenPrebid: () => this.openCabanaPrebid(),
30926          clViewPrebidHistory: () => { const history = document.getElementById('cabana-prebid-history'); if (!history) return; history.focus({ preventScroll:true }); history.scrollIntoView({ behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches ?'auto' : 'smooth', block:'start' }); },
30927          clPrebidModalOpen: !!s.cabanaPrebidModalOpen,
30928          clPrebidSuccessOpen: !!s.cabanaPrebidSuccessFx,
30929          clPrebidSuccessEyebrow: s.cabanaPrebidSuccessFx?.eyebrow || '',
30930          clPrebidSuccessTitle: s.cabanaPrebidSuccessFx?.title || '',
30931          clPrebidSuccessAmount: s.cabanaPrebidSuccessFx?.amount || '',
30932          clPrebidSuccessMessage: s.cabanaPrebidSuccessFx?.message || '',
30933          clClosePrebid: () => this.closeCabanaPrebid(),
30934          clPrebidCuit: this.cuitInputDisplay(selectedPrebidForm.cuitCuil),
30935          clSetPrebidCuit: (event) => this.setCabanaPrebidField('cuitCuil', this.cuitInputRaw(event.target.value)),
30936          clPrebidAmount: this.moneyInputDisplay(selectedPrebidForm.amount || selectedPrebidSuggested),
30937          clPrebidAmountRoll: this.rollInteger(this.cabanaPrebidNumber(selectedPrebidForm.amount || selectedPrebidSuggested)),
30938          clPrebidRolling: !!s.cabanaPrebidRolling,
30939          clPrebidRollOpacity: s.cabanaPrebidRolling ?'1' : '0',
30940          clPrebidRollTransition: s.cabanaPrebidRolling ?'none' : 'opacity 120ms ease',
30941          clSetPrebidAmount: (event) => this.setCabanaPrebidField('amount', this.moneyInputRaw(event.target.value)),
30942          clPrebidMinus: () => this.adjustCabanaPrebidAmount(-1),
30943          clPrebidPlus: () => this.adjustCabanaPrebidAmount(1),
30944          clPrebidCommission: !!selectedPrebidForm.enComision,
30945          clPrebidCommissionBg: selectedPrebidForm.enComision ?'#1f8a67' : '#d7e1eb',
30946          clPrebidCommissionLeft: selectedPrebidForm.enComision ?'25px' : '4px',
30947          clTogglePrebidCommission: () => this.setCabanaPrebidField('enComision', !selectedPrebidForm.enComision),
30948          clPrebidCommissionFor: selectedPrebidForm.enComisionPara || '',
30949          clSetPrebidCommissionFor: (event) => this.setCabanaPrebidField('enComisionPara', event.target.value),
30950          clPrebidGrouped: selectedPrebidGrouped,
30951          clPrebidNeedsCuit: !s.cabanaPrebidProfile?.cuitCuil,
30952          clPrebidCertMode: current?.prebid?.preofertaIndividual === true ?'Elegí un animal.' : 'Elegí uno o más animales, consecutivos o no.',
30953          clPrebidCertOptions: selectedPrebidCerts.map((cert) => {
30954            const isSelected = (selectedPrebidForm.selectedIdentifiers || []).map(String).includes(String(cert));
30955            return {
30956              cert,
30957              role: current?.prebid?.preofertaIndividual === true ?'radio':'checkbox',
30958              label: this.cabanaPrebidIdentifierLabel(cert),
30959              selected: isSelected,
30960              bg: isSelected ?'#e8f7ef' : '#fff',
30961              border: isSelected ?'#1f8a67' : '#cdd9e5',
30962              color: isSelected ?'#087653' : '#50647a',
30963              icon: isSelected ?'assets/icons/lucide/check.svg' : 'assets/icons/lucide/circle-dollar-sign.svg',
30964              cursor: 'pointer',
30965              toggle: () => this.toggleCabanaPrebidCert(String(cert)),
30966            };
30967          }),
30968          clPrebidHasCerts: selectedPrebidCerts.length > 0,
30969          clPrebidHasMessage: !!s.cabanaPrebidMessage || !prebidWindow.active,
30970          clPrebidMessage: !prebidWindow.active ? (prebidWindow.upcoming ? 'Las preofertas todavía no comenzaron.' : 'Las preofertas están cerradas.') : window.CabanaPrebid.errorMessage(s.cabanaPrebidMessage?.text || '', ''),
30971          clPrebidMessageBg: s.cabanaPrebidMessage?.ok ?'#e7f7ef' : '#fff0f1',
30972          clPrebidMessageColor: s.cabanaPrebidMessage?.ok ?'#087653' : '#b4232f',
30973          clPrebidSubmitting: !!s.cabanaPrebidSubmitting || !this.cabanaPrebidCuitReady() || !prebidWindow.active,
30974          clCuitCheckVisible:!!s.cabanaPrebidCuitCheck, clCuitCheckText:s.cabanaPrebidCuitCheck?.text || '',
30975          clCuitCheckError:s.cabanaPrebidCuitCheck?.status === 'error',
30976          clCuitCheckColor:s.cabanaPrebidCuitCheck?.status === 'error' ? '#b4232f' : '#087653',
30977          clRetryCuit:()=>this.checkCabanaPrebidCuit(s.cabanaPrebidProfile?.cuitCuil || s.cabanaPrebidForm?.cuitCuil),
30978          clPrebidSubmitLabel: s.cabanaPrebidSubmitting ?'Enviando...' : s.cabanaPrebidCuitCheck?.status === 'loading' ? 'Validando CUIT…' : 'Confirmar preoferta',
30979          clPrebidSubmitOpacity: s.cabanaPrebidSubmitting || !this.cabanaPrebidCuitReady() || !prebidWindow.active ?'.58' : '1',
30980          clSubmitPrebid: () => this.submitCabanaPrebid(),
30981          cabanaDetailCols: detailStacked ?'1fr' : 'minmax(0, 1fr) minmax(280px, 360px)', cabanaSticky: detailStacked ?'static' : 'sticky',
30982        };
30983      })(),
30984      ...(() => {
30985        const remate = s.cabanaLotCreateRemate;
30986        const schema = Array.isArray(remate?.formsByCategory) ?remate.formsByCategory : [];
30987        const form = s.cabanaLotCreateForm || { categoryId: '', values: {}, observations: '' };
30988        const participantCabanas = remate ?this.cabanaRemateParticipants(remate) : [];
30989        const categoryKey = (category) => String(category?.categoryId || category?.categoryName || '');
30990        const selectedCategory = schema.find((category) => categoryKey(category) === String(form.categoryId)) || schema[0];
30991        const subforms = (selectedCategory?.forms || []).filter((subform) => subform.isActive !== false).sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)).map((subform, formIndex) => ({
30992          number: formIndex + 1, name: subform.name || 'Datos del lote',
30993          fields: (subform.fields || []).sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map((field, fieldIndex) => {
30994            const key = categoryKey(selectedCategory) + ':' + formIndex + ':' + fieldIndex;
30995            const type = String(field.type || 'Texto').toLowerCase();
30996            const value = form.values?.[key] ?? (type === 'booleano' ? false : '');
30997            return { label: field.name || 'Campo', value, placeholder: 'Completar ' + String(field.name || 'campo').toLowerCase(), options: field.options || [], isPrimary: !!field.isPrimaryIdentifier, isText: type === 'texto', isNumber: type === 'numero' || type === 'número', isDate: type === 'fecha', isSelect: type === 'seleccion' || type === 'selección', isBoolean: type === 'booleano' || type === 'sí / no', on: (event) => this.setCabanaLotValue(key, event.target.value, type === 'texto' || type === 'numero' || type === 'número'), toggle: () => this.setCabanaLotValue(key, !value), track: value ?'#1f8a70' : '#cfd8e3', knob: value ?'15px' : '0px', booleanLabel: value ?'Sí' : 'No' };
30998          }),
30999        }));
31000        const editing = !!s.cabanaLotEditingCert;
31001        const loading = !!s.cabanaLotCreateLoading;
31002        const saving = !!s.cabanaLotCreateSaving;
31003        return {
31004          cabanaLotHasCabanas: participantCabanas.length > 0, cabanaLotNoCabanas: participantCabanas.length === 0,
31005          cabanaLotCabanas: participantCabanas.map((cabana) => { const active = cabana.key === String(form.cabanaKey || ''); const brand = cabana.brandColor || '#0a5ca8'; return { ...cabana, logo: cabana.logoDataUrl, hasLogo: !!cabana.logoDataUrl, noLogo: !cabana.logoDataUrl, initial: cabana.name.charAt(0).toUpperCase(), brand, select: () => this.setCabanaLotCabana(cabana.key), border: active ?brand : '#dce5ef', bg: active ?'#f4f9fd' : '#fff', shadow: active ?'0 0 0 2px ' + brand + '22' : 'none', checkBorder: active ?brand : '#bdcad8', checkBg: active ?brand : '#fff', check: active ?'?' : '' }; }),
31006          cabanaLotRemateTitle: remate?.nombre_remate || remate?.titulo || remate?.title || 'Remate de cabaña',
31007          cabanaLotPageTitle: editing ?'Editar lote' : 'Crear lote', cabanaLotModeLabel: editing ?('CERT. ' + s.cabanaLotEditingCert) : 'BORRADOR', cabanaLotBackLabel: editing ?'Volver al lote' : 'Volver a Cabañas',
31008          cabanaLotCreateLoading: loading, cabanaLotNoSchema: !loading && schema.length === 0, cabanaLotHasSchema: !loading && schema.length > 0,
31009          cabanaLotCategories: schema.map((category) => { const active = categoryKey(category) === categoryKey(selectedCategory); return { label: category.categoryName || 'Categoría', select: () => this.setCabanaLotCategory(categoryKey(category)), bg: active ?'#002e5f' : '#f5f8fb', color: active ?'#fff' : '#3d4c5f', border: active ?'#002e5f' : '#dce5ef' }; }),
31010          cabanaLotSubforms: subforms, cabanaLotObservations: form.observations || '', onCabanaLotObservations: (event) => this.setState({ cabanaLotCreateForm: { ...form, observations: event.target.value } }),
31011          cabanaLotImages: (s.cabanaLotImages || []).map((media) => ({ ...media, remove: () => this.removeCabanaLotMedia('image', media.id) })), cabanaLotHasImages: (s.cabanaLotImages || []).length > 0, cabanaLotImageCount: (s.cabanaLotImages || []).length,
31012          cabanaLotVideos: (s.cabanaLotVideosUpload || []).map((media) => ({ ...media, remove: () => this.removeCabanaLotMedia('video', media.id) })), cabanaLotHasVideos: (s.cabanaLotVideosUpload || []).length > 0, cabanaLotVideoCount: (s.cabanaLotVideosUpload || []).length,
31013          onCabanaLotImages: (event) => { this.addCabanaLotMedia('image', event.target.files); event.target.value = ''; }, onCabanaLotVideos: (event) => { this.addCabanaLotMedia('video', event.target.files); event.target.value = ''; }, saveCabanaLotDraft: () => this.saveCabanaLotDraft(), closeCabanaLotCreate: () => this.closeCabanaLotCreate(),
31014          cabanaLotCreateSaving: saving, cabanaLotSaveLabel: saving ?(editing ?'Guardando cambios...' : 'Guardando lote...') : (editing ?'Guardar cambios' : 'Guardar lote'), cabanaLotSaveOpacity: saving ?'.62' : '1', cabanaLotSaveCursor: saving ?'wait' : 'pointer',
31015          cabanaLotLayout: s.isMobile ?'1fr' : 'minmax(0,1.45fr) minmax(300px,.72fr)', cabanaLotMediaPosition: s.isMobile ?'static' : 'sticky',
31016        };
31017      })(),
31018      navItems: navDefs.filter((n) => n.route !== 'profile').map((n) => ({
31019        label: n.label, path: n.path, go: () => this.go(n.route),
31020        bg: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#fff' : 'transparent', color: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#002e5f' : '#c8d8ea',
31021        lightBg: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#002e5f' : 'transparent', lightColor: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#fff' : '#4a5c72',
31022        mobColor: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#e63946' : '#8593a6', mobWeight: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?3 : 2, weight: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?700 : 500,
31023      })),
31024      mobileNavItems: (s.publicAdvanceMode ?[] : navDefs.filter((n) => ['home', 'pool', 'remates', 'cabanas', 'profile'].includes(n.route))).map((n) => ({
31025        label: n.label, go: () => this.go(n.route),
31026        current: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'page' : 'false',
31027        mobColor: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#002e5f' : '#718096',
31028        iconBg: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#e7f0fa' : 'transparent',
31029        mobilePath: {
31030          home: 'M3 10.5 12 3l9 7.5M5 9v10a2 2 0 0 0 2 2h3v-7h4v7h3a2 2 0 0 0 2-2V9',
31031          pool: 'M4 3h7a2 2 0 0 1 1.4.6l8 8a2 2 0 0 1 0 2.8l-6 6a2 2 0 0 1-2.8 0l-8-8A2 2 0 0 1 3 11V4a1 1 0 0 1 1-1ZM7.5 7.5h.01',
31032          remates: 'm14 3 7 7-4 4-7-7 4-4ZM12 9l-8 8a2.1 2.1 0 0 0 3 3l8-8M14 21h8M16 18h4v3',
31033          cabanas: 'M8 6C5 6 4 4 4 2M16 6c3 0 4-2 4-4M7 8 2 6c0 3 2 5 5 5M17 8l5-2c0 3-2 5-5 5M7 8c0-4 10-4 10 0v6c0 4-2 7-5 7s-5-3-5-7V8ZM8 17c1-2 7-2 8 0M9.5 10.5v1M14.5 10.5v1M10 18h.01M14 18h.01',
31034          profile: 'M20 21v-2a5 5 0 0 0-5-5H9a5 5 0 0 0-5 5v2M16 6a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z',
31035        }[n.route],
31036      })),
31037      accessCards: [
31038        { chipBg: '#002e5f', tint: '#edf3fa', icon: 'm14 4 6 6-4 4-6-6 4-4ZM13 3l8 8M9 7l8 8M12 10l-8 8a1.4 1.4 0 0 0 2 2l8-8M13 21h9M15 18h5v3', iconImage: 'assets/home-icons/remates-v2.png', title: 'Remates', text: 'La agenda del mercado', go: () => this.go('remates') },
31039        { chipBg: '#c93248', tint: '#fdf0f2', icon: 'M4 4h7l9 9a2 2 0 0 1 0 3l-4 4a2 2 0 0 1-3 0l-9-9V4ZM8 7a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM12 12l4 4', iconImage: 'assets/home-icons/roslot-v2.png', title: 'Roslot', text: 'Explorá lotes y ofertas', go: () => this.go('pool') },
31040        { chipBg: '#0a5ca8', tint: '#edf5fc', icon: 'M8 6C5 6 5 3 5 2m11 4c3 0 3-3 3-4M7 8 2 6c0 4 2 6 5 5m10-3 5-2c0 4-2 6-5 5M7 8c0-4 10-4 10 0v6c0 4-2 7-5 7s-5-3-5-7V8Zm2 9c1-2 5-2 6 0M9.5 10.5v1m5-1v1', iconImage: 'assets/home-icons/cabanas-v3.png', title: 'Cabañas', text: 'Genética y reproductores', go: () => this.go('cabanas') },
31041        { id: 'feria', chipBg: '#1f7a63', tint: '#edf7f2', icon: 'M5 5.5h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3ZM16 10l4.5-2.7a1 1 0 0 1 1.5.85v7.7a1 1 0 0 1-1.5.85L16 14M6.5 12h5M9 9.5v5', iconImage: 'assets/home-icons/feria-v2.png', title: 'Crear feria', text: 'Creá y gestioná tu transmisión', go: () => this.go('feria') },
31042      ].filter((card) => card.title !== 'Roslot' || this.canAccessRoslot(s.apiUser))
31043        .filter((card) => card.id !== 'feria' || this.canAccessFeria(s.apiUser)),
31044      agendaCards: upcoming.map((fa, i) => {
31045        const isToday = this.feriaDate(fa)?.toDateString() === todayKey;
31046        const consRef = this.remateConsignatariaRef(fa) || 'Rosgan';
31047        const consName = this.consRecord(consRef)?.name || this.consRecord(consRef)?.nombre || consRef;
31048        const whiteConsLogo = this.consWhiteLogo(consRef);
31049        const consLogo = whiteConsLogo || this.consLogo(consRef);
31050        const background = this.consBackground(consRef);
31051        const brandColor = this.consBrandColor(consRef);
31052        return {
31053          title: fa.title || fa.nombre || 'Remate Rosgan', meta: this.feriaLabel(fa) + ' · ' + (fa.lugar || fa.consignataria || 'Rosgan'),
31054          href: fa.youtubeUrl || 'https://www.youtube.com/@rosgantv',
31055          thumb: background || (fa.youtubeUrl ?this.ytThumb(fa.youtubeUrl) : ['assets/feria.png', 'assets/cabanas.png', 'assets/angus-field.png'][i % 3]),
31056          consName, consLogo, hasConsLogo: !!consLogo, consLogoFilter: whiteConsLogo ?'none' : 'brightness(0) invert(1)', brandColor, thumbPosition: this.consBackgroundPosition(consRef, 'card'),
31057          thumbOverlay: this.rgba(this.shade(brandColor, -0.06), 0.82),
31058          badge: isToday ?'HOY' : (this.feriaDate(fa)?.toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) || 'PRÓXIMO'),
31059          badgeBg: isToday ?'#e63946' : '#fff', badgeColor: isToday ?'#fff' : '#0a5ca8',
31060        };
31061      }),
31062      hasVideos: vids.length > 0,
31063      videoCards: vids.map((v) => {
31064        const vid = v.id || v.videoId || this.ytId(v.url);
31065        return {
31066          title: v.title || 'Rosgan TV', href: v.url || (vid ?'https://www.youtube.com/watch?v=' + vid : '#'),
31067          thumb: v.thumbnailUrl || v.thumbnail || (vid ?'https://i.ytimg.com/vi/' + vid + '/hqdefault.jpg' : 'assets/feria.png'),
31068          meta: v.publishedAt ?new Date(v.publishedAt).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Rosgan TV',
31069        };
31070      }),
31071      cafeText: s.cafe?.title || s.cafe?.description?.split('\n')[0] || 'Noticias del campo, entrevistas y actualidad ganadera todas las semanas.',
31072      cafeHref: s.cafe?.url || (s.cafe?.id ?'https://www.youtube.com/playlist?list=' + s.cafe.id : 'https://www.youtube.com/playlist?list=PLAa_PiRkH0KOCs4VtYFA7NpUeuKs_rt4f'),
31073      hasLoteNoticias: loteCards.length > 0, loteCards: loteCards.map((l) => ({ ...l, badgeBg: l.featured ?'#e63946' : 'rgba(255,255,255,0.16)' })),
31074      heroIndices: homeIndexCards,
31075      hasNoticias: !!noticiaLead, noticiaLead: noticiaLead || {}, noticiaCards,
31076      hasNotiCarousel: noticiaCards.length > 0, notiCurrent: notiCurrent || {}, notiKey: 'n' + notiIdx, notiDots, hasNotiDots: noticiaCards.length > 1,
31077      noticiasGrid: s.isMobile ?'1fr' : 'minmax(0, 1.5fr) minmax(300px, 1fr)',
31078      leadHoverIn: (e) => { const im = e.currentTarget.querySelector('[data-lead-img]'); if (im) im.style.transform = 'scale(1.06)'; },
31079      leadHoverOut: (e) => { const im = e.currentTarget.querySelector('[data-lead-img]'); if (im) im.style.transform = 'scale(1)'; },
31080      channelCards: consList.map((c, i) => {
31081        const name = c.name || c.nombre || c.slug || 'Canal';
31082        const logo = this.consWhiteLogo(name) || this.consLogo(name) || c.logoUrl || c.logo || c.imageUrl || '';
31083        const background = c.backgroundUrl || c.bannerUrl || this.consBackgroun
31083d(name);
31084        return { name, initial: name.trim()[0].toUpperCase(), chipBg: this.consBrandColor(name) || accents[i % accents.length], logo, hasLogo: !!logo, noLogo: !logo, background, hasBackground: !!background, color: this.consBrandColor(name), href: CONS_WEBS[name] };
31085      }),
31086      toast: s.toast, toastBottom: s.isMobile ?'96px' : '28px',
31087      // ---- pool ----
31088      poolBarTop: s.isMobile ?'0px' : '61px',
31089      showRoslotOperations: !!s.user && (roslotCanBuy || roslotCanSell),
31090      roslotMobilePanelClass: s.roslotMobilePanelOpen ?'is-mobile-open' : '',
31091      roslotMobileDockClass: s.roslotMobilePanelOpen ?'is-hidden' : '',
31092      openRoslotMobilePanel: () => this.openRoslotMobilePanel(),
31093      closeRoslotMobilePanel: () => this.closeRoslotMobilePanel(),
31094      roslotCanBuy,
31095      roslotCanSell,
31096      roslotKpis,
31097      roslotOperations,
31098      roslotHasOperations: roslotOperations.length > 0,
31099      roslotOperationsEmpty: roslotOperations.length === 0,
31100      roslotWhatsappToggleClass: [
31101        s.whatsappNotificationsEnabled ?'is-on' : 'is-off',
31102        !s.whatsappNotificationsAvailable ?'is-unavailable' : '',
31103        (s.whatsappNotificationsSaving || s.whatsappNotificationsLoading) ?'is-busy' : '',
31104      ].filter(Boolean).join(' '),
31105      roslotWhatsappAriaChecked: s.whatsappNotificationsEnabled ?'true' : 'false',
31106      roslotWhatsappStatus: s.whatsappNotificationsSaving ?'Guardando…'
31107        : s.whatsappNotificationsLoading ?'Consultando…'
31108        : !s.whatsappNotificationsAvailable ?'Verificá tu teléfono para activarlas'
31109        : s.whatsappNotificationsEnabled ?'Activadas' : 'Desactivadas',
31110      roslotWhatsappTitle: s.whatsappNotificationsMessage || (!s.whatsappNotificationsAvailable
31111        ?'Verificá tu teléfono para recibir avisos por WhatsApp.'
31112        : s.whatsappNotificationsEnabled ?'Desactivar notificaciones por WhatsApp.' : 'Activar notificaciones por WhatsApp.'),
31113      roslotWhatsappBusy: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading,
31114      toggleRoslotWhatsapp: () => this.toggleWhatsAppNotifications(),
31115      roslotDailyCategories,
31116      roslotHasDailyCategories: roslotDailyCategories.length > 0,
31117      roslotDailyCategoriesEmpty: roslotDailyCategories.length === 0,
31118      roslotDailyCategoriesLabel: roslotDailyCategories.length === 1 ?'1 categoría' : roslotDailyCategories.length + ' categorías',
31119      roslotActivity,
31120      roslotHasActivity: roslotActivity.length > 0,
31121      roslotActivityLoading: roslotActivity.length === 0 && !Array.isArray(s.notificationHistory),
31122      roslotActivityEmpty: roslotActivity.length === 0 && Array.isArray(s.notificationHistory),
31123      roslotRealtimeLabel: roslotActivity.length === 1 ?'1 evento de hoy' : roslotActivity.length + ' eventos de hoy',
31124      showRoslotLiveToast: s.route === 'pool' && s.isMobile && !!roslotLiveToastItem,
31125      roslotLiveToastTitle: roslotLiveToastItem?.title || '',
31126      roslotLiveToastBody: roslotLiveToastItem?.body || '',
31127      roslotLiveToastIcon: roslotLiveToastItem?.icon || 'assets/icons/lucide/bell-ring.svg',
31128      roslotLiveToastAria: [roslotLiveToastItem?.title, roslotLiveToastItem?.body].filter(Boolean).join('. '),
31129      openRoslotLiveToast: () => this.openRoslotLiveToast(),
31130      showPoolOfferTracker: false,
31131      poolOfferTrackerText: activeStandingCount
31132        ?(activeStandingCount === 1 ?'Tenés 1 posición activa en el pool.' : 'Tenés ' + activeStandingCount + ' posiciones activas en el pool.')
31133        : 'No tenés ofertas activas en los lotes abiertos.',
31134      poolQuery: s.poolQuery, onPoolQuery: (e) => this.bufferInputState('pool-query', { poolQuery: e.target.value }, 100),
31135      poolSort: s.poolSort, onSort: (e) => this.setState({ poolSort: e.target.value }),
31136      setPoolGridView: () => this.setState({ poolView: 'grid' }),
31137      setPoolListView: () => this.setState({ poolView: 'list' }),
31138      poolViewClass: s.poolView === 'list' ?'is-list' : 'is-grid',
31139      poolGridPressed: s.poolView !== 'list' ?'true' : 'false',
31140      poolListPressed: s.poolView === 'list' ?'true' : 'false',
31141      poolGridViewBg: s.poolView !== 'list' ?'#002e5f' : 'transparent',
31142      poolGridViewColor: s.poolView !== 'list' ?'#fff' : '#8593a6',
31143      poolListViewBg: s.poolView === 'list' ?'#002e5f' : 'transparent',
31144      poolListViewColor: s.poolView === 'list' ?'#fff' : '#8593a6',
31145      toggleFilters: () => this.setState({ filtersOpen: !s.filtersOpen }),
31146      filtersOpen: s.filtersOpen, activeFilterCount,
31147      filterSidebarClass: s.filtersOpen ?'is-mobile-open' : '',
31148      filterBtnBg: s.filtersOpen || activeFilterCount ?'#002e5f' : '#fff',
31149      filterBtnColor: s.filtersOpen || activeFilterCount ?'#fff' : '#112033',
31150      filterBtnBorder: s.filtersOpen || activeFilterCount ?'#002e5f' : '#e3eaf2',
31151      filterIconFilter: s.filtersOpen || activeFilterCount ?'none' : 'invert(40%) sepia(12%) saturate(800%) hue-rotate(175deg)',
31152        marketStatusLabel: marketStatus.label,
31153        marketStatusBg: marketStatus.bg,
31154        marketStatusColor: marketStatus.color,
31155        marketStatusBorder: marketStatus.border,
31156        marketStatusIcon: marketStatus.icon,
31157        marketCountdownLabel: s.lotsLoading ?'Sincronizando horarios…' : (marketOpen ?(marketTarget ?'Cierra en ' + marketRemainingLabel(marketTarget) : 'Operando ahora') : (marketTarget ?'Abre en ' + marketRemainingLabel(marketTarget) : 'Sin subastas programadas')),
31158        marketScheduleLabel: s.lotsLoading ?'Consultando la próxima ventana del mercado' : (marketOpen ?(marketTarget ?'Cierre previsto: ' + marketTargetLabel : 'Mercado operando en este momento') : (marketTarget ?'Próxima apertura: ' + marketTargetLabel : 'Próxima apertura sin programar')),
31159        marketTimerColor: marketOpen ?'#176c58' : marketTarget ?'#07589d' : '#687a8f',
31160        marketProgressValue,
31161        marketProgress: marketProgressValue + '%',
31162        marketProgressBg: marketOpen ?'linear-gradient(90deg,#1f7a63,#43b48f)' : marketTarget ?'linear-gradient(90deg,#075da8,#28a7d8)' : '#aebdca',
31163      statusTabs: statusDefs.map((t) => {
31164        const count = poolEligibleLots.filter((lot) => isPoolDay(lot, t.key)).length;
31165        const active = s.poolStatus === t.key;
31166        return { label: t.label, count, live: !!t.live, go: () => this.setState({ poolStatus: t.key }),
31167          bg: active ?'#002e5f' : '#fff', color: active ?'#fff' : '#3d4c5f', border: active ?'#002e5f' : '#e3eaf2' };
31168      }),
31169      filterGroups: [
31170        { label: 'CATEGORÍA', options: mkOptions('category', filterS
31170ourceLots.map((l) => l.categoria)) },
31171        { label: 'DESTINO', options: mkOptions('destination', filterSourceLots.map((l) => l.destino)) },
31172        { label: 'ENGORDE', options: mkOptions('fattening', filterSourceLots.map((l) => l.engorde)) },
31173        { label: 'PROVINCIA', options: mkOptions('province', filterSourceLots.map((l) => l.provincia)) },
31174      ].filter((g) => g.options.length),
31175      toggleChina: () => this.setState({ poolFilters: { ...f, china: !f.china } }),
31176      chinaBg: f.china ?'#9d2235' : '#f2f6fb', chinaColor: f.china ?'#fff' : '#3d4c5f', chinaBorder: f.china ?'#9d2235' : '#dde7f2',
31177      clearFilters: () => this.setState({ poolFilters: { category: [], destination: [], fattening: [], province: [], china: false }, poolQuery: '' }),
31178      resultCount: filtered.length, resultLabel: filtered.length === 1 ?'lote encontrado' : 'lotes encontrados',
31179      lotsSourceNote: '',
31180      lotsLoading: s.lotsLoading, skeletons: Array.from({ length: 6 }, (_, i) => ({ i })),
31181      roslotPresenceBuyers: s.roslotPresenceError && !s.roslotPresence ?'—' :(s.roslotPresenceLoading && !s.roslotPresence ?'…' :String(s.roslotPresence?.buyers ?? 0)),
31182      roslotPresenceSellers: s.roslotPresenceError && !s.roslotPresence ?'—' :(s.roslotPresenceLoading && !s.roslotPresence ?'…' :String(s.roslotPresence?.sellers ?? 0)),
31183      roslotPresenceBuyerLabel: Number(s.roslotPresence?.buyers) === 1 ?'comprador' :'compradores',
31184      roslotPresenceSellerLabel: Number(s.roslotPresence?.sellers) === 1 ?'vendedor' :'vendedores',
31185      showLots: !s.lotsLoading && filtered.length > 0, showEmpty: !s.lotsLoading && !!s.lots && filtered.length === 0,
31186      poolEmptyTitle: s.poolStatus === 'upcoming' ?'No hay lotes en próximas subastas' : 'No hay lotes en la subasta de hoy',
31187      poolEmptyText: activeFilterCount || q ?'Probá limpiar algún filtro o cambiar la búsqueda.' : (s.poolStatus === 'upcoming' ?'Los próximos lotes aparecerán acá cuando sean publicados.' : 'El mercado no tiene lotes programados para hoy.'),
31188      lotCards: filtered.map((l) => ({
31189        cert: l.cert, categoria: l.categoria, origen: l.origen, image: l.image,
31190        videoPreview: l.videoPreviewUrl, isLive: l.status === 'En vivo', isSold: l.status === 'Vendido' || l.auctionStatus === 'sold',
31191        base: this.fmtARS(l.baseNum), condicion: l.condicion, cabezas: String(l.cabezas), peso: l.peso,
31192        ...(() => {
31193          const offerCount = Math.max(Number(l.bidCount) || 0, (s.lotBids?.[l.cert] || []).length);
31194          const hasOffers = offerCount > 0 || !!l.currentBidId || !!l.currentBidBuyerUid;
31195          const isSold = l.status === 'Vendido' || l.auctionStatus === 'sold';
31196          const current = Number(isSold ?l.soldAmount : l.currentBidAmount) || Number(l.currentBidAmount) || Number(l.baseNum) || 0;
31197          const base = Number(l.baseNum) || 0;
31198          const variation = base ?((current - base) / base) * 100 : 0;
31199          const direction = variation > 0.005 ?'up' : variation < -0.005 ?'down' : 'flat';
31200          return {
31201            hasOffers,
31202            priceBadgeClass: hasOffers ?'has-offers' : 'is-base',
31203            priceLabel: isSold ?'PRECIO FINAL' : hasOffers ?'ÚLTIMA OFERTA' : 'OFERTA BASE',
31204            currentPrice: this.fmtARS(current),
31205            priceChange: direction === 'up' || direction === 'down' ?Math.abs(variation).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '%' : '0%',
31206            priceChangeUp: direction === 'up', priceChangeDown: direction === 'down',
31207            priceChangeColor: direction === 'up' ?'#bdf4de' : direction === 'down' ?'#ffd0d5' : '#dbe6f2',
31208            priceChangeBg: direction === 'up' ?'rgba(31,122,99,0.58)' : direction === 'down' ?'rgba(230,57,70,0.58)' : 'rgba(255,255,255,0.12)',
31209          };
31210        })(),
31211        razas: (l.razas.length ?l.razas : ['Sin razas']).join(', '),
31212        destino: l.destino, destinoColor: l.destino === 'Faena' ?'#9d2235' : l.destino === 'Invernada' ?'#1f7a63' : '#0a5ca8',
31213        plazo: l.plazo, plazoDetalle: l.plazoOtro, showPlazoDetalle: l.plazoEsOtro && !!l.plazoOtro, aptoChina: l.aptoChina,
31214        open: () => this.openLot(l.cert), openBid: () => this.openLot(l.cert, true),
31215        hoverIn, hoverOut,
31216        standing: l.auctionStatus === 'open' && !!standingByCert[l.cert], standingLabel: standingByCert[l.cert]?.label || '',
31217        standingColor: standingByCert[l.cert]?.color || '#1f7a63', standingBg: standingByCert[l.cert]?.bg || 'rgba(31,122,99,0.14)',
31218        standingWinning: standingByCert[l.cert]?.key === 'winning', standingOutbid: standingByCert[l.cert]?.key === 'outbid',
31219      })),
31220      // ---- lot detail ----
31221      lotTitle: lot?.categoria || '', lotCert: lot?.cert || '', lotOrigen: lot?.origen || '',
31222      lotStatus: lot?.status === 'Vendido' && lot.fechaDisponibleCarga && lot.fechaDisponibleCarga <= this.localIsoDate() ?'Listo para la carga' : (lot?.status || ''),
31223      lotStatusBg: lot?.status === 'Vendido' ?'#1f7a63' : lot?.status === 'En vivo' ?'#e63946' : lot?.status === 'Próximo' ?'#0a5ca8' : '#65758a',
31224      lotStatusIcon: 'assets/icons/lucide/' + (lot?.status === 'Vendido' ?'circle-check-big' : lot?.status === 'En vivo' ?'radio' : lot?.status === 'Próximo' ?'clock-3' : 'circle-x') + '.svg',
31225      lotLayoutCols: s.isMobile ?'1fr' : 'minmax(0, 1.55fr) minmax(320px, 1fr)',
31226      bidPanelPos: s.isMobile ?'static' : 'sticky',
31227      lotVideoUrl: selVideo?.url || '', lotNoVideo: !selVideo?.url, lotImage: lot?.image || 'assets/angus-field.png',
31228      lotVideoPlay: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true),
31229      lotVideoPause: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true),
31230      lotVideoMeta: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true),
31231      lotVideoTimeUpdate: (event) => this.syncRoslotLotVideoState(event?.currentTarget, false),
31232      lotVideoVolumeChange: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true),
31233      lotVideoEnded: (event) => {
31234        const video = event?.currentTarget;
31235        const count = lot?.videos?.length || 0;
31236        if (count > 1) {
31237          this.setState({ selectedVideo: (this.state.selectedVideo + 1) % count, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true });
31238        } else if (video) {
31239          video.currentTime = 0;
31240          video.play?.().catch(() => {});
31241          this.syncRoslotLotVideoState(video, true);
31242        }
31243      },
31244      lotTogglePlay: (event) => this.toggleRoslotLotVideoPlayback(event),
31245      lotSeekVideo: (event) => this.seekRoslotLotVideo(event),
31246      lotToggleMute: (event) => this.toggleRoslotLotVideoMute(event),
31247      lotSetVolume: (event) => this.setRoslotLotVideoVolume(event),
31248      lotFullscreen: (event) => this.openRoslotLotVideoFullscreen(event),
31249      lotPlayIcon: 'assets/icons/lucide/' + (s.lotVideoPlaying ?'pause' : 'play') + '.svg',
31250      lotPlayLabel: s.lotVideoPlaying ?'Pausar video' : 'Reproducir video',
31251      lotVolumeIcon: 'assets/icons/lucide/' + (s.lotVideoMuted ?'volume-x' : 'volume-2') + '.svg',
31252      lotMuteLabel: s.lotVideoMuted ?'Activar sonido' : 'Silenciar video',
31253      lotVideoProgress: Math.round(Number(s.lotVideoProgress || 0) * 10) / 10,
31254      lotVideoVolume: Math.round(Number(s.lotVideoVolume || 0) * 100) / 100,
31255      lotVideoTime: this.formatCabanaVideoTime((Number(s.lotVideoProgress || 0) / 100) * Number(s.lotVideoDuration || 0)) + ' / ' + this.formatCabanaVideoTime(s.lotVideoDuration),
31256      hasVideoThumbs: (lot?.videos.length || 0) > 1,
31257      videoThumbs: (lot?.videos || []).map((v, i) => {
31258        const generatedThumb = v.thumb || s.videoThumbByUrl[v.url] || '';
31259        const fallbackThumb = lot?.image || 'assets/angus-field.png';
31260        const thumb = generatedThumb || fallbackThumb;
31261        return {
31262          n: i + 1,
31263          url: v.url,
31264          thumb,
31265          hasThumb: !!thumb,
31266          loading: !thumb && !s.videoThumbFailedByUrl[v.url],
31267          border: i === s.selectedVideo ?'#e63946' : 'transparent',
31268          select: () => this.setState({ selectedVideo: i, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true }),
31269        };
31270      }),
31271      lotGroups: lot ? [
31272        { label: 'CANTIDAD Y PESO', cards: [
31273          { label: 'CABEZAS', value: String(lot.cabezas) }, { label: 'PESO PROM.', value: lot.peso },
31274          { label: 'PESO MÍNIMO', value: lot.pesoMin }, { label: 'PESO MÁXIMO', value: lot.pesoMax } ] },
31275        { label: 'COMERCIAL', cards: [
31276          { label: 'PRECIO BASE', value: this.fmtARS(lot.baseNum) }, { label: 'CONDICIÓN', value: lot.condicion },
31277          { label: 'PLAZO', value: lot.plazo }, ...(lot.plazoEsOtro && lot.plazoOtro ?[{ label: 'DETALLE DEL PLAZO', value: lot.plazoOtro }] : []), { label: 'PESAJE', value: lot.pesaje },
31278          { label: 'DISPONIBLE PARA CARGA', value: lot.fechaDisponibleCarga ?this.formatBaDate(lot.fechaDisponibleCarga) : 'Sin fecha informada' } ] },
31279        { label: 'OPERACIÓN Y SANIDAD', cards: [
31280          { label: 'ENGORDE', value: lot.engorde }, { label: 'DESTINO', value: lot.destino },
31281          { label: 'APTO CHINA', value: lot.aptoChina ?'Sí' : 'No' }, { label: 'DESBASTE', value: lot.desbaste },
31282          { label: 'RENDIMIENTO', value: lot.rendimiento }, { label: 'RAZAS', value: (lot.razas.length ?lot.razas : ['—']).join(', ') } ] },
31283      ] : [],
31284      lotObs: lot?.obs || '',
31285      lotPublisherName,
31286      lotPublisherLogo,
31287      lotPublisherHasLogo: !!lotPublisherLogo,
31288      lotPublisherNoLogo: !lotPublisherLogo,
31289      lotPublisherInitials: lotHasConsignataria
31290        ?lotPublisherName.split(/\s+/).filter(Boolean).slice(0, 2).map((word) => word[0]?.toUpperCase() || '').join('')
31291        : (lotPublisherPending ?'…' : 'CAF'),
31292      lotPublisherAvatarBg: lotPublisherLogo && !lotPublisherUsesWhiteLogo ?'#fff' : '#002e5f',
31293      lotViews: lot ? (() => {
31294        const count = Object.prototype.hasOwnProperty.call(s.lotViewCounts, lot.cert)
31295           ? s.lotViewCounts[lot.cert]
31296          : lot.views;
31297        return anim ?this.rollInteger(count) : Number(count || 0).toLocaleString('es-AR');
31298      })() : '',
31299      lotBase: lot ?this.fmtARS(lot.baseNum) : '', lotCondicion: lot?.condicion || '',
31300      lotBidSuccess: !!lot && s.roslotBidSuccessFx?.cert === String(lot.cert || ''),
31301      lotBidSuccessAmount: !!lot && s.roslotBidSuccessFx?.cert === String(lot.cert || '') ?this.fmtARS(s.roslotBidSuccessFx.amount) : '',
31302      ...(() => {
31303        const cert = lot?.cert || '';
31304        const raw = (s.lotBids[cert] || []);
31305        const loading = !!s.lotBidsLoading[cert] && !raw.length;
31306        return {
31307          lotBidsLoading: loading,
31308          lotBidsHas: raw.length > 0,
31309          lotBidsCount: String(raw.length),
31310          lotBidRows: raw.slice(0, 8).map((b, i) => {
31311            const m = this.bidStatusMeta(b.status);
31312            return { amount: this.fmtARS(b.amount), bidder: 'Oferta reservada', date: b.date ?new Date(b.date).toLocaleDateString('es-AR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '', status: m.label, statusBg: m.bg, statusColor: m.color, top: i === 0 };
31313          }),
31314        };
31315      })(),
31316      currentBidRoll: anim && lot ?this.rollNumber(curBid) : (lot ?this.fmtARS(curBid) : ''),
31317      bidTargetRoll: anim && lot ?this.rollNumber(target) : (lot ?this.fmtARS(target) : ''),
31318      ...(() => {
31319        const averageWeight = Number(lot?.pesoNum) || Number(String(lot?.peso || '').replace(/[^\d.,-]/g, '').replace(',', '.')) || 0;
31320        const quantity = Number(lot?.cabezas) || 0;
31321        const condition = String(lot?.condicion || '');
31322        const isPerHead = /cabeza|head/i.test(condition);
31323        const isPerKg = /kg|kilo|kilogram/i.test(condition);
31324        const isPerMeat = !isPerHead && !isPerKg && /carne|gancho|rinde|rendimiento/i.test(condition);
31325        const yieldPercent = Number(lot?.raw?.rendimientoEstimado ?? lot?.raw?.estimatedYield ?? lot?.raw?.rinde) || 0;
31326        const unitCost = target > 0 && (isPerHead || averageWeight > 0)
31327          ? (isPerHead ? target : isPerMeat ? (yieldPercent > 0 ? target * averageWeight * yieldPercent / 100 : 0) : target * averageWeight)
31328          : 0;
31329        return {
31330          bidPriceUnitLabel: isPerHead ? 'por cabeza' : isPerMeat ? 'por kg carne' : 'por kg vivo',
31331          bidEstimatedUnitCost: unitCost > 0 ?(anim ?this.rollNumber(unitCost) : this.fmtARS(unitCost)) : '—',
31332          bidEstimatedTotalCost: unitCost > 0 && quantity > 0 ?(anim ?this.rollNumber(unitCost * quantity) : this.fmtARS(unitCost * quantity)) : '—',
31333          bidEstimatedNote: isPerMeat && yieldPercent <= 0 ?'Falta el rendimiento para estimar el costo. Importes + IVA.' : isPerMeat ?'Calculado con ' + yieldPercent.toLocaleStr
31333ing('es-AR') + '% de rendimiento. Importes + IVA.' : 'Valores estimados. Importes + IVA.',
31334        };
31335      })(),
31336      bidOffsetLabel: adj.offset === 0 ?'Tu oferta (sin ajuste)' : 'Tu oferta (' + (adj.offset > 0 ?'+' : '−') + '$' + Math.abs(adj.offset).toLocaleString('es-AR') + ')',
31337      bidPlus: () => lot && this.setBidAdj(lot.cert, { ...adj, offset: adj.offset + adj.increment }),
31338      bidMinus: () => lot && this.setBidAdj(lot.cert, { ...adj, offset: Math.max(adj.offset - adj.increment, -curBid) }),
31339      plusBg: 'rgba(31,122,99,0.75)', minusBg: 'rgba(230,57,70,0.55)',
31340      bidIncrements: this.INCREMENTS.map((inc) => ({
31341        label: '+$' + inc, pick: () => lot && this.setBidAdj(lot.cert, { increment: inc, offset: adj.offset === adj.increment ?inc : adj.offset }),
31342        bg: adj.increment === inc ?'#fff' : 'rgba(255,255,255,0.08)', color: adj.increment === inc ?'#002e5f' : '#dbe6f2',
31343        border: adj.increment === inc ?'#fff' : 'rgba(255,255,255,0.22)',
31344      })),
31345      ...(() => {
31346        const isOpen = lot?.auctionStatus === 'open';
31347        const isSold = lot?.auctionStatus === 'sold' || lot?.status === 'Vendido';
31348        const isScheduled = lot?.auctionStatus === 'scheduled' || lot?.status === 'Próximo';
31349        const isOwnLot = !!(lot?.sellerUid && s.user?.uid && lot.sellerUid === s.user.uid);
31350        const hasOffer = !!(lot?.currentBidId || lot?.currentBidBuyerUid || Number(lot?.bidCount) > 0 || (s.lotBids?.[lot?.cert] || []).length > 0);
31351        const isLeading = !!(lot && isOpen && !isOwnLot && (
31352          (lot.currentBidBuyerUid && String(lot.currentBidBuyerUid) === String(s.user?.uid || ''))
31353          || standingByCert[lot.cert]?.key === 'winning'
31354        ));
31355        const closing = !!(lot?.cert && s.lotClosingCert === lot.cert);
31356        const canClose = !!(lot && isOpen && isOwnLot && hasOffer && !closing && s.lotsReal);
31357        const canBid = !!(lot && isOpen && !isOwnLot && !isLeading && !s.bidSubmitting && s.lotsReal);
31358        return {
31359          bidPriceTitle: isSold ?'PRECIO FINAL' : 'OFERTA VIGENTE',
31360          bidPriceSubtitle: isSold ?'Lote vendido · ' + lot.condicion : 'Base configurada: ' + this.fmtARS(lot?.baseNum || 0) + ' · ' + (lot?.condicion || ''),
31361          bidPrimaryAction: () => isOwnLot ?this.closeRoslotLot(lot?.cert) : this.submitBid(),
31362          bidDisabled: isOwnLot ?!canClose : !canBid,
31363          bidControlsVisible: canBid,
31364          bidControlsOpacity: canBid ?'1' : '0.42',
31365          bidControlsPointer: canBid ?'auto' : 'none',
31366          showSellerHammerPanel: isOwnLot && isOpen,
31367          sellerHammerMessage: hasOffer
31368            ? 'Si esta oferta ya te convence, podés bajar el martillo. El lote se cerrará y la oferta líder quedará adjudicada.'
31369            : 'Cuando el lote reciba una oferta válida, vas a poder bajar el martillo y adjudicarlo antes del cierre general.',
31370          showWinningNotice: isLeading,
31371          bidShowsHammer: isOwnLot && ((isOpen && hasOffer) || isSold),
31372          bidBtnLabel: closing
31373             ? 'Bajando el martillo…'
31374            : s.bidSubmitting
31375             ? 'Enviando oferta…'
31376            : isSold
31377               ? 'Subasta cerrada'
31378              : isScheduled
31379                 ? 'Subasta aún no abierta'
31380                : !isOpen
31381                   ? 'Subasta cerrada'
31382                  : isOwnLot
31383                     ? (hasOffer ?'Bajar el martillo' : 'Esperando ofertas')
31384                    : isLeading
31385                       ? '¡Vas ganando!'
31386                    : 'Ofertar ' + (lot ?this.fmtARS(target) : ''),
31387          bidBtnBg: isSold || isLeading ?'#1f7a63' : canClose ?'#b86d00' : canBid ?'#e63946' : isScheduled ?'#0a5ca8' : '#65758a',
31388          bidBtnCursor: canBid || canClose ?'pointer' : 'default',
31389          bidBtnOpacity: '1',
31390          bidBtnAnim: anim && (canBid || canClose) ?'bidPulse 2.4s ease-out infinite' : 'none',
31391        };
31392      })(),
31393      bidMessage: s.bidMessage?.text || '', bidMsgBg: s.bidMessage?.ok ?'rgba(95,214,143,0.18)' : 'rgba(230,57,70,0.25)',
31394      bidMsgColor: s.bidMessage?.ok ?'#bff0d3' : '#ffd3d7',
31395      estRows: lot ? [
31396        { label: 'Nombre', value: lot.establecimiento }, { label: 'Localidad', value: lot.localidad },
31397        { label: 'Provincia', value: lot.provincia || 'Pendiente' }, { label: 'Comitente', value: lot.comitente },
31398      ] : [],
31399      // ---- load wizard ----
31400      lotDestinationOpen:!!s.lotDestinationOpen,lotDestinationLoading:!!s.lotDestinationLoading,lotDestinationError:s.lotDestinationError||'',
31401      lotDestinationRoslot:this.canAccessRoslot()&&canLoadLot,
31402      lotDestinationChoices:(s.lotDestinationItems||[]).filter(item=>this.lotDestinationAllowed(item)).map(item=>({title:item.titulo||item.nombre_remate||item.name||'Remate '+item.id,type:item.cabanasConfig?'Cabañas':'Remate Rosgan',logos:this.lotDestinationLogos(item),choose:()=>this.chooseLotDestination(item)})),
31403      lotDestinationEmpty:!s.lotDestinationLoading&&!s.lotDestinationError&&!(s.lotDestinationItems||[]).some(item=>this.lotDestinationAllowed(item)),
31404      goMobileLoad: () => ['remateDetail', 'cabanaDetail'].includes(s.route) ? this.openLotDestination() : this.beginNewLot(),
31405      goLotDestination:()=>this.openLotDestination(),closeLotDestination:()=>this.setState({lotDestinationOpen:false}),
31406      lotDestinationKey:event=>{if(event.key==='Escape')this.setState({lotDestinationOpen:false});},
31407      chooseRoslotDestination:()=>{if(!this.canAccessRoslot()||!canLoadLot)return;this.setState({lotDestinationOpen:false});this.beginNewLot();},
31408      goLoad: () => this.beginNewLot(),
31409      resetForm: () => this.resetLotWizard(),
31410      wizardBack: () => this.closeLotWizard(),
31411      wizardBackLabel: s.editLotCert ?'Volver a Mis lotes' : 'Volver al pool',
31412      wizardTitle: s.editLotCert ?'Editar lote · Cert. ' + s.editLotCert : 'Cargar nuevo lote',
31413      wizardSubtitle: s.editLotCert ?'Actualizá los datos y videos del lote antes de guardar.' : 'Cuatro etapas con previsualización en vivo antes de publicar.',
31414      wizardResetLabel: s.editLotCert ?'Restablecer cambios' : 'Limpiar formulario',
31415      loadLayoutCols: s.isMobile ?'1fr' : 'minmax(0, 1.6fr) minmax(280px, 1fr)',
31416      stepKey: 'step-' + s.loadStep,
31417      isStep0: s.loadStep === 0, isStep1: s.loadStep === 1, isStep2: s.loadStep === 2, isStep3: s.loadStep === 3,
31418      wizardSteps: this.WIZ_META.map((w, i) => {
31419        const done = this.loadStepComplete(i); const active = i === s.loadStep;
31420        return { title: w.title, text: w.text, go: () => this.setState({ loadStep: i, formMessage: null }),
31421          barBg: active ?'#e63946' : done ?'#1f7a63' : '#e3eaf2',
31422          dotBg: active ?'#002e5f' : done ?'#1f7a63' : '#fff', dotColor: (active || done) ?'#fff' : '#8593a6',
31423          dotBorder: active ?'#002e5f' : done ?'#1f7a63' : '#dde7f2', done: done && !active, numberVisible: !done || active, number: String(i + 1),
31424          titleColor: active ?'#112033' : '#4a5c72' };
31425      }),
31426      estSelectorHas: (s.establishments || []).length > 0,
31427      estSelectOptions: (s.establishments || []).map((e) => ({ id: e.id, label: e.nombre + (e.localidad ?' · ' + e.localidad : '') })),
31428      selectedEstId: fm.estId || '',
31429      estIsSaved: !!fm.estId,
31430      pickEst: (e) => this.setEst(e.target.value),
31431      ...(() => {
31432        const ef = s.estForm || this.emptyEstForm();
31433        const cf = s.comitenteForm || this.emptyComitenteForm();
31434        return {
31435          wzEstOpen: s.estFormOpen,
31436          wzEstToggle: () => { if (!this.canManageComitentes(s.apiUser)) return; const opening = !s.estFormOpen; if (opening) this.fetchProvinces(); this.setState({ estFormOpen: opening, estForm: s.estFormOpen ?s.estForm : this.emptyEstForm(), estFormMessage: null }); },
31437          wzNoComitentes: !s.comitentesLoading && !(s.comitentes || []).length,
31438          wzComitenteOpen: !!s.comitenteFormOpen,
31439          wzToggleComitente: () => { const opening = !s.comitenteFormOpen; this.setState({ comitenteFormOpen: opening, comitenteForm: opening ?this.emptyComitenteForm() : s.comitenteForm, comitenteFormMessage: null, comitenteLookupLoading: false }); },
31440          wz_cf_alias: cf.alias || '', wz_set_cf_alias: e => this.setComitenteForm('alias', e.target.value),
31441          wz_cf_cuit: cf.cuitCuil || '', wz_set_cf_cuit: (e) => this.onComitenteCuit(e.target.value),
31442          wz_cf_razon: cf.nombreFiscal || '', wz_cf_provincia: cf.provincia || '', wz_cf_localidad: cf.localidad || '',
31443          wzComitenteLookup: !!s.comitenteLookupLoading,
31444          wzComitenteMsg: s.comitenteFormMessage?.text || '',
31445          wzComitenteMsgBg: s.comitenteFormMessage?.ok ?'#e8f7f1' : '#fff0f1', wzComitenteMsgColor: s.comitenteFormMessage?.ok ?'#17664f' : '#b42335',
31446          wzSaveComitente: () => this.saveComitente(),
31447          wzComitenteSaveDisabled: !!s.comitenteSaving || !!s.comitenteLookupLoading || !cf.source,
31448          wzComitenteSaveOpacity: (s.comitenteSaving || s.comitenteLookupLoading || !cf.source) ?'.52' : '1',
31449          wzComitenteSaveLabel: s.comitenteSaving ?'Creando…' : (s.comitenteLookupLoading ?'Consultando…' : 'Guardar comitente'),
31450          wz_ef_comitenteId: ef.comitenteId || '', wz_set_ef_comitenteId: (e) => this.setEstForm('comitenteId', e.target.value),
31451          wzEfComitenteOpts: (s.comitentes || []).map((item) => ({ id: item.id, label: (item.alias || item.nombreFiscal || 'Sin razón social') + ' · ' + (item.cuitCuil || 'Sin CUIT') })),
31452          wz_ef_nombre: ef.nombre, wz_set_ef_nombre: (e) => this.setEstForm('nombre', e.target.value),
31453          wz_ef_renspa: ef.renspa || '', wz_set_ef_renspa: (e) => this.setEstForm('renspa', e.target.value),
31454          wz_ef_provincia: ef.provincia, wz_set_ef_provincia: (e) => this.setEstForm('provincia', e.target.value),
31455          wz_ef_localidad: ef.localidad, wz_set_ef_localidad: (e) => this.onEstPlaceInput(e.target.value),
31456          wz_ef_latitud: ef.latitud, wz_set_ef_latitud: (e) => this.setEstForm('latitud', e.target.value),
31457          wz_ef_longitud: ef.longitud, wz_set_ef_longitud: (e) => this.setEstForm('longitud', e.target.value),
31458          wzEfProvinceOpts: s.geoProvinces || [], wzEfProvPlaceholder: s.geoLoadingProv ?'Cargando provincias…' : 'Seleccionar…',
31459          wzEfLocalityOpts: (s.geoLocs[ef.provincia] || []).map((l) => l.nombre),
31460          wzEfLocDisabled: !ef.provincia, wzEfLocBg: !ef.provincia ?'#f2f6fb' : '#fff',
31461          wzEfLocPlaceholder: !ef.provincia ?'Elegí provincia primero' : 'Escribí una localidad',
31462          wzPlaceLoading: !!s.estPlaceLoading,
31463          wzPlaceResultsOpen: !!(s.estPlaceResults || []).length,
31464          wzPlaceResults: (s.estPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectEstPlace(item) })),
31465          wzOpenPlaceResults: () => { if (ef.provincia && String(ef.localidad || '').trim().length >= 3) this.searchEstPlaces(ef.localidad, ef.provincia); },
31466          wzClosePlaceResults: () => this.closeEstPlaceResults(),
31467          wzPlaceError: s.estPlaceError || '',
31468          wzEfCoordsNote: (ef.latitud && ef.longitud) ?'Coordenadas autocompletadas desde la localidad' : 'Se completan al elegir la localidad',
31469          wzSaveEst: () => this.saveEstablishment(true),
31470          wzEstSaveLabel: s.estSaving ?'Guardando…' : 'Guardar y usar',
31471          wzEstSaveDisabled: s.estSaving,
31472          wzEstSaveCursor: s.estSaving ?'not-allowed' : 'pointer',
31473          wzEstSaveOpacity: s.estSaving ?'0.55' : '1',
31474          wzEstFormMsg: s.estFormMessage?.text || '', wzEstFormMsgBg: s.estFormMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', wzEstFormMsgColor: s.estFormMessage?.ok ?'#1f7a63' : '#c22',
31475        };
31476      })(),
31477      f_estName: fm.estName, set_estName: (e) => this.setF('estName', e.target.value),
31478      f_comitente: fm.comitente, set_comitente: (e) => this.setF('comitente', e.target.value),
31479      f_lat: fm.lat, set_lat: (e) => this.setF('lat', e.target.value),
31480      f_lon: fm.lon, set_lon: (e) => this.setF('lon', e.target.value),
31481      f_fecha: fm.fecha, set_fecha: (e) => this.setF('fecha', e.target.value),
31482      f_prov: fm.prov, set_prov: (e) => this.setF('prov', e.target.value),
31483      f_loc: fm.loc, set_loc: (e) => this.setF('loc', e.target.value),
31484      f_cat: fm.cat, set_cat: (e) => {
31485        const categoryId = e.target.value;
31486        this.setState({ form: { ...this.form(), cat: categoryId, subcat: '', rinde: this.catYieldPct(categoryId) }, formMessage: null });
31487      },
31488      f_subcat: fm.subcat, set_subcat: (e) => this.setF('subcat', e.target.value),
31489      subcatOptions: this.subcatsFor(fm.cat).map(([id, label]) => ({ id, label })),
31490      subcatHas: this.subcatsFor(fm.cat).length > 0,
31491      f_cant: fm.cant, set_cant: (e) => this.setF('cant', e.target.value),
31492      catIsMixed: this.isMixedCat(fm.cat), catNotMixed: !this.isMixedCat(fm.cat),
31493      f_cantMachos: fm.cantMachos, set_cantMachos: (e) => this.setF('cantMachos', e.target.value),
31494      f_cantHembras: fm.cantHembras, set_cantHembras: (e) => this.setF('cantHembras', e.target.value),
31495      cantLabel: this.isMixedCat(fm.cat) ?'Cantidad total (opcional)' : 'Cantidad de cabezas',
31496      categoryOptions: this.CATALOGS.categories.map(([id, label]) => ({ id, label })),
31497      razasCount: fm.razas.length,
31498      breedOptions: this.CATALOGS.breeds.map(([id, label]) => ({ label, toggle: () => this.toggleRaza(id),
31499        bg: fm.razas.includes(id) ?'#002e5f' : '#f2f6fb', color: fm.razas.includes(id) ?'#fff' : '#3d4c5f',
31500        border: fm.razas.includes(id) ?'#002e5f' : '#dde7f2' })),
31501      fatteningOptions: opt('fatteningTypes', 'engorde'),
31502      showDiasEncierre: !!fm.engorde && fm.engorde !== 'a-campo',
31503      f_diasEncierre: fm.diasEncierre, set_diasEncierre: (e) => this.setF('diasEncierre', e.target.value),
31504      destinationOptions: opt('destinations', 'destino').filter((option) => !((fm.engorde === 'racion' || fm.engorde === 'feedlot') && option.id === 'exportacion-hilton')),
31505      hiltonRestricted: fm.engorde === 'racion' || fm.engorde === 'feedlot',
31506      chinaOptions: [['true', 'Sí'], ['false', 'No']].map(([id, label]) => ({ label, pick: () => this.setF('china', id),
31507        bg: fm.china === id ?(id === 'true' ?'#9d2235' : '#002e5f') : '#f2f6fb', color: fm.china === id ?'#fff' : '#3d4c5f',
31508        border: fm.china === id ?(id === 'true' ?'#9d2235' : '#002e5f') : '#dde7f2' })),
31509      conditionOptions: opt('saleConditions', 'cond'),
31510      weighingOptions: opt('weighingOptions', 'pesaje'),
31511      termOptions: opt('termOptions', 'plazo'),
31512      showPlazoOtro: fm.plazo === 'otro',
31513      f_plazoOtro: fm.plazoOtro, set_plazoOtro: (e) => this.setF('plazoOtro', e.target.value),
31514      f_pesoProm: fm.pesoProm, set_pesoProm: (e) => this.setF('pesoProm', e.target.value),
31515      f_pesoMin: fm.pesoMin, set_pesoMin: (e) => this.setF('pesoMin', e.target.value),
31516      f_pesoMax: fm.pesoMax, set_pesoMax: (e) => this.setF('pesoMax', e.target.value),
31517      f_precio: this.moneyInputDisplay(fm.precio), set_precio: (e) => this.setF('precio', this.moneyInputRaw(e.target.value)),
31518      f_listoParaCargar: fm.listoParaCargar === true,
31519      f_listoLabel: fm.listoParaCargar === true ?'✓ Listo para cargar' : '○ Listo para cargar',
31520      f_listoBg: fm.listoParaCargar === true ?'#e6f5ef' : '#f5f8fc', f_listoBorder: fm.listoParaCargar === true ?'#1f7a63' : '#dce6f0', f_listoColor: fm.listoParaCargar === true ?'#17664f' : '#3d4c5f',
31521      toggleListoParaCargar: () => this.setF('listoParaCargar', fm.listoParaCargar !== true),
31522      f_showFechaCarga: fm.listoParaCargar !== true,
31523      f_dispoMin: this.loadDateOffset(fm.fecha, 1), f_dispoMax: this.loadDateOffset(fm.fecha, 7),
31524      f_dispo: fm.dispo, set_dispo: (e) => this.setF('dispo', e.target.value),
31525      f_desbaste: fm.desbaste, set_desbaste: (e) => { let v = String(e.target.value).replace(/\D/g, '').slice(0, 3); if (v !== '' && Number(v) > 100) v = '100'; this.setF('desbaste', v); },
31526      f_rinde: fm.rinde, set_rinde: (e) => { const raw = String(e.target.value || '').replace(',', '.').replace(/[^\d.]/g, ''); const value = Number(raw) > 100 ?'100' : raw; this.setF('rinde', value); },
31527      f_obs: fm.obs, set_obs: (e) => this.setF('obs', e.target.value),
31528      videoSlots: fm.videos.map((v, i) => {
31529        const uploading = s.vidProg[i] != null;
31530        const filled = !!v && !uploading;
31531        const prog = Math.min(100, Math.round(s.vidProg[i] || 0));
31532        const isLotCover = fm.thumbIdx === i;
31533        const frames = v?.frames || [];
31534        const chosen = Math.max(0, frames.findIndex((frame) => frame === v?.selectedFrame || frame?.previewUrl === v?.selectedFrame?.previewUrl));
31535        return { n: i + 1, filled, empty: !filled && !uploading, uploading,
31536          progress: prog, progressPct: prog + '%', poster: v?.selectedFrame?.previewUrl || frames[0]?.previewUrl || '',
31537          upload: () => this.startVideoUpload(i),
31538          remove: (e) => { e.stopPropagation(); this.removeVideo(i); },
31539          isLotCover, coverLabel: isLotCover ?'Portada del lote' : 'Marcar como portada del lote',
31540          coverBg: isLotCover ?'#002e5f' : '#fff', coverColor: isLotCover ?'#fff' : '#0a5ca8', coverBorder: isLotCover ?'#002e5f' : '#cddbec',
31541          setLotCover: () => this.setF('thumbIdx', i),
31542          posterOpts: frames.map((frame, p) => ({ url: frame.previewUrl, selected: chosen === p,
31543            pick: () => this.setVideoPoster(i, p),
31544            ring: chosen === p ?'#0a5ca8' : 'transparent' })) };
31545      }),
31546      uploadAllVideos: () => this.uploadAllVideos(),
31547      filmAssistedVideo: () => this.filmAssistedVideo(),
31548      videosAnyPending: fm.videos.filter(Boolean).length < 3 && Object.keys(s.vidProg || {}).length === 0,
31549      formMessage: s.formMessage?.text || '',
31550      formMsgBg: s.formMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)',
31551      formMsgColor: s.formMessage?.ok ?'#1f7a63' : '#c22',
31552      prevStep: () => this.setState({ loadStep: Math.max(s.loadStep - 1, 0), formMessage: null }),
31553      nextStep: () => this.loadNext(),
31554      prevVisible: s.loadStep === 0 ?'hidden' : 'visible',
31555      nextBg: s.loadStep === 3 ?'#e63946' : '#002e5f',
31556      nextLabel: s.publishing ?(s.editLotCert ?'Guardando…' : 'Publicando…') : s.loadStep === 3 ?(s.editLotCert ?'Guardar cambios' : 'Publicar lote') : 'Continuar',
31557      nextShowsArrow: !s.publishing && s.loadStep < 3,
31558      showPreviewCard: !s.isMobile,
31559      pv_image: (wizFilledVideos.length ?(fm.videos[fm.thumbIdx] ?this.videoPoster(fm.thumbIdx) : this.videoPoster(fm.videos.findIndex(Boolean))) : 'assets/angus-field.png'),
31560      pv_cert: 'Nuevo',
31561      pv_cat: this.catName(fm.cat) || 'Categoría del lote', pv_origen: [fm.loc, fm.prov].filter(
31561Boolean).join(', ') || 'Origen del lote',
31562      pv_precio: fm.precio ?this.fmtARS(Number(fm.precio)) : '$ —', pv_cond: this.labelOf('saleConditions', fm.cond) || 'Condición',
31563      pv_cabezas: this.totalHeads(fm) || '—', pv_peso: fm.pesoProm ?fm.pesoProm + ' kg' : '—',
31564      pv_razas: fm.razas.length ?fm.razas.map((id) => this.labelOf('breeds', id)).join(', ') : '—',
31565      pv_destino: this.labelOf('destinations', fm.destino) || '—',
31566      pv_completeness: this.formCompleteness() + '%', pv_china: fm.china === 'true',
31567      goProfile: () => this.go('profile'),
31568      ...(() => {
31569        // Dashboard values always reflect current API state; animation must never zero live data.
31570        const P = 1;
31571        const all = s.lots || [];
31572        const userUid = String(s.user?.uid || '');
31573        const userEmail = String(s.user?.email || '').toLowerCase();
31574        const ownFromAll = all.filter((l) => (userUid && l.sellerUid && l.sellerUid === userUid) || (userEmail && String(l.sellerEmail || '').toLowerCase() === userEmail));
31575        const mine = (Array.isArray(s.myLots) && s.myLots.length) ?s.myLots : ownFromAll;
31576        const fmtK = (n) => Math.round(n).toLocaleString('es-AR');
31577        const totalCabezas = mine.reduce((a, l) => a + (Number(l.cabezas) || 0), 0);
31578        const bidFor = (l) => Math.max(
31579          Number(l.baseNum) || 0,
31580          Number(l.currentBidAmount) || 0,
31581          Number(l.soldAmount) || 0,
31582          Number(s.bidsPlaced[l.cert]) || 0,
31583          ...(l.bids || []).map((bid) => Number(bid?.amount || bid) || 0),
31584          0
31585        );
31586        const cartera = mine.reduce((a, l) => a + bidFor(l) * (l.condicion && /cabeza/i.test(l.condicion) ?(Number(l.cabezas) || 1) : (Number(l.cabezas) || 1) * (l.pesoNum || 1)), 0);
31587        const placedEntries = Object.entries(s.bidsPlaced);
31588        const offerLots = placedEntries.length ?placedEntries.map(([cert, amt]) => ({ lot: all.find((l) => l.cert === cert), amt })).filter((o) => o.lot) : [];
31589        const fallbackImg = '';
31590        const profileBidByCert = {};
31591        (s.myBids || []).forEach((bid) => {
31592          if (!bid.cert) return;
31593          const current = profileBidByCert[bid.cert];
31594          if (!current || Number(bid.amount) >= Number(current.amount)) profileBidByCert[bid.cert] = { ...bid };
31595          else profileBidByCert[bid.cert] = { ...current, topAmount: Math.max(Number(current.topAmount) || 0, Number(bid.topAmount) || 0) };
31596        });
31597        const profileBids = Object.values(profileBidByCert);
31598        const offerData = profileBids.length
31599           ? profileBids.map((b) => {
31600               const lot = all.find((l) => l.cert === b.cert); const m = this.bidStatusMeta(b.status);
31601               const isOpen = lot?.auctionStatus === 'open';
31602               const standing = isOpen && !failedBidPattern.test(b.status) ?standingByCert[b.cert] : null;
31603               const displayAmount = Number(b.amount) || 0;
31604               const topAmount = Number(standing?.topAmount || lot?.currentBidAmount || b.topAmount || displayAmount) || 0;
31605               const isSold = lot?.auctionStatus === 'sold';
31606               const winnerUid = String(lot?.winnerBuyerUid || '');
31607               const isWinner = isSold && (winnerUid ?winnerUid === String(s.user?.uid || '') : m.label === 'Aprobada');
31608               return { open: () => this.openLot(b.cert), image: lot?.image || fallbackImg, categoria: lot?.categoria || b.lotName || ('Lote ' + b.cert), cert: b.cert, origen: lot?.origen || (b.date ?new Date(b.date).toLocaleDateString('es-AR') : ''), amount: this.fmtARS(displayAmount), amountNum: displayAmount, topAmount, best: this.fmtARS(topAmount), status: m.label, statusBg: m.bg, statusColor: m.color, auctionStatus: lot?.auctionStatus || '', isSold, isWinner, standing: !!standing, standKey: standing?.key || '', standLabel: standing?.label || '', standBg: standing?.bg || '', standColor: standing?.color || '' };
31609            })
31610          : offerLots.map(({ lot, amt }) => {
31611              const standing = standingByCert[lot.cert];
31612               return { open: () =>
31612 this.openLot(lot.cert), image: lot.image, categoria: lot.categoria, cert: lot.cert, origen: lot.origen, amount: this.fmtARS(amt), amountNum: Number(amt) || 0, topAmount: Number(standing?.topAmount || amt) || 0, best: this.fmtARS(standing?.topAmount || amt), status: 'Activa', statusBg: 'rgba(10,92,168,0.14)', statusColor: '#0a5ca8', auctionStatus: lot.auctionStatus || '', isSold: lot.auctionStatus === 'sold', standing: !!standing, standKey: standing?.key || '', standLabel: standing?.label || '', standBg: standing?.bg || '', standColor: standing?.color || '' };
31613            });
31614        const liveOffers = offerData.filter((offer) => offer.standing).map((offer) => {
31615          const adjustment = this.bidAdj(offer.cert);
31616          const marketAmount = Math.max(offer.topAmount, offer.amountNum);
31617          const target = marketAmount + adjustment.offset;
31618          const winning = offer.standKey === 'winning';
31619          const standLabel = winning ?'Vas ganando' : 'Te superaron';
31620          const standColor = winning ?'#1f7a63' : '#c22';
31621          const standBg = winning ?'rgba(31,122,99,0.14)' : 'rgba(230,57,70,0.14)';
31622          const submitting = s.quickBidCert === offer.cert;
31623          return {
31624            cert: offer.cert, image: offer.image, categoria: offer.categoria, origen: offer.origen,
31625            myRoll: anim ?this.rollNumber(offer.amountNum) : this.fmtARS(offer.amountNum),
31626            marketRoll: anim ?this.rollNumber(marketAmount) : this.fmtARS(marketAmount),
31627            winning, outbid: !winning, showOutbidIcon: !winning,
31628            standLabel, standColor, standBg,
31629            open: offer.open,
31630            offsetLabel: '+$' + adjustment.offset.toLocaleString('es-AR'),
31631            targetRoll: anim ?this.rollNumber(target) : this.fmtARS(target),
31632            incOpts: this.INCREMENTS.map((increment) => ({
31633              label: '+$' + increment,
31634              activeBg: adjustment.increment === increment ?'#002e5f' : '#fff',
31635              activeColor: adjustment.increment === increment ?'#fff' : '#3d4c5f',
31636              activeBorder: adjustment.increment === increment ?'#002e5f' : '#dde7f2',
31637              pick: () => this.setBidAdj(offer.cert, { increment, offset: Math.max(increment, adjustment.offset) }),
31638            })),
31639            plus: () => this.setBidAdj(offer.cert, { ...adjustment, offset: adjustment.offset + adjustment.increment }),
31640            minus: () => this.setBidAdj(offer.cert, { ...adjustment, offset: Math.max(adjustment.increment, adjustment.offset - adjustment.increment) }),
31641            confirm: (event) => { event.stopPropagation(); this.quickBid(offer.cert, marketAmount); },
31642            submitting, submitOpacity: submitting ?'0.6' : '1',
31643            confirmLabel: submitting ?'Enviando…' : 'Ofertar ' + this.fmtARS(target),
31644          };
31645        });
31646        const completedOffers = offerData
31647          .filter((offer) => offer.isWinner)
31648          .map((offer) => ({
31649            cert: offer.cert,
31650            image: offer.image,
31651             categoria: offer.categoria,
31652             origen: offer.origen,
31653             amount: offer.amount,
31654             amountNum: offer.amountNum,
31655              open: offer.open,
31656           }));
31657        const buyerClosedAllRows = offerData
31658          .filter((offer) => ['sold', 'closed_no_bids'].includes(offer.auctionStatus))
31659          .map((offer) => ({
31660            ...offer,
31661            resultLabel: offer.isWinner ?'Ganaste' : 'No adjudicada',
31662            resultBg: offer.isWinner ?'rgba(31,122,99,0.14)' : 'rgba(101,117,138,0.12)',
31663            resultColor: offer.isWinner ?'#1f7a63' : '#65758a',
31664            amountLabel: offer.isWinner ?'OFERTA ACEPTADA' : 'MI OFERTA FINAL',
31665          }));
31666        const buyerClosedPages = Math.max(1, Math.ceil(buyerClosedAllRows.length / 10));
31667        const buyerClosedPage = Math.min(Math.max(1, Number(s.buyerClosedPage) || 1), buyerClosedPages);
31668        const buyerClosedOfferRows = buyerClosedAllRows.slice((buyerClosedPage - 1) * 10, buyerClosedPage * 10);
31669        const sellerOpenLots = mine.filter((lot) => lot.auctionStatus === 'open');
31670        const sellerTrackedLots = mine.filter((lot) => ['open', 'sold'].includes(lot.auctionStatus));
31671        const sellerOfferRows = sellerTrackedLots.map((lot) => {
31672          const bids = (s.lotBids[lot.cert] || []).slice().sort((a, b) => b.amount - a.amount);
31673          const isFinal = lot.auctionStatus === 'sold';
31674          const targetBidId = isFinal ?lot.winnerBidId : lot.currentBidId;
31675          const targetBuyerUid = isFinal ?lot.winnerBuyerUid : lot.currentBidBuyerUid;
31676          const leader = bids.find((bid) => targetBidId && bid.id === targetBidId)
31677            || bids.find((bid) => targetBuyerUid && bid.buyerUid === targetBuyerUid)
31678            || bids[0]
31679            || null;
31680          const amount = isFinal
31681            ? Math.max(Number(lot.soldAmount) || 0, Number(leader?.amount) || 0)
31682            : Math.max(Number(lot.currentBidAmount) || 0, Number(leader?.amount) || 0);
31683          const base = Number(lot.baseNum) || 0;
31684          const variation = base ?((amount - base) / base) * 100 : 0;
31685          const direction = variation > 0.005 ?'up' : variation < -0.005 ?'down' : 'flat';
31686          const count = Math.max(Number(lot.bidCount) || 0, bids.length);
31687          const buyer = String(leader?.bidder || (isFinal ?lot.raw?.winnerBuyerName : lot.raw?.currentBidBuyerName) || '').trim() || 'Sin nombre informado';
31688          return {
31689            cert: lot.cert,
31690            image: lot.image,
31691            categoria: lot.categoria,
31692            origen: lot.origen,
31693            buyer,
31694            buyerLabel: isFinal ?'COMPRADOR GANADOR' : 'COMPRADOR LÍDER',
31695            base: this.fmtARS(base),
31696            amount: this.fmtARS(amount),
31697            amountLabel: isFinal ?'PRECIO FINAL' : 'OFERTA LÍDER',
31698            amountNum: amount,
31699            isFinal,
31700            variation: direction === 'up' || direction === 'down' ?Math.abs(variation).toLocaleStr
31700ing('es-AR', { maximumFractionDigits: 1 }) + '%' : '0%',
31701            variationUp: direction === 'up', variationDown: direction === 'down',
31702            variationColor: direction === 'up' ?'#1f7a63' : direction === 'down' ?'#c22' : '#65758a',
31703            variationBg: direction === 'up' ?'rgba(31,122,99,0.14)' : direction === 'down' ?'rgba(230,57,70,0.14)' : 'rgba(101,117,138,0.12)',
31704            count: count === 1 ?'1 oferta' : count + ' ofertas',
31705            countNum: count,
31706            open: () => this.openLot(lot.cert),
31707          };
31708        }).filter((row) => row.amountNum > 0 || row.countNum > 0);
31709        const sellerOpenOfferRows = sellerOfferRows.filter((row) => !row.isFinal);
31710        const sellerClosedAllRows = sellerOfferRows.filter((row) => row.isFinal);
31711        const sellerClosedPages = Math.max(1, Math.ceil(sellerClosedAllRows.length / 10));
31712        const sellerClosedPage = Math.min(Math.max(1, Number(s.sellerClosedPage) || 1), sellerClosedPages);
31713        const sellerClosedOfferRows = sellerClosedAllRows.slice((sellerClosedPage - 1) * 10, sellerClosedPage * 10);
31714        const pendData = (s.pendingBids || []).map((b) => {
31715          const lot = all.find((l) => l.cert === b.cert);
31716          return { id: b.id, cert: b.cert, image: lot?.image || fallbackImg, categoria: lot?.categoria || b.lotName || ('Lote ' + b.cert), origen: lot?.origen || '', amount: this.fmtARS(b.amount), bidder: 'Identidad reservada', approveLabel: s.bidActioning === b.id ?'Aprobando…' : 'Aprobar', disabled: !!s.bidActioning, open: () => this.openLot(b.cert), approve: () => this.actOnBid(b.cert, b.id, 'approve'), reject: () => this.actOnBid(b.cert, b.id, 'reject') };
31717        });
31718        const estList = s.establishments || [];
31719        const ef = s.estForm || this.emptyEstForm();
31720        const comitenteForm = s.comitenteForm || this.emptyComitenteForm();
31721        const comitenteDirectory = this.profileComitenteDirectory();
31722        const comitenteData = comitenteDirectory.rows.map((item) => ({
31723          editAlias: () => this.editComitenteAlias(item.id), id: item.id, fiscalName: item.nombreFiscal || 'Nombre fiscal no informado', razonSocial: item.alias || item.nombreFiscal || 'Sin razón social',
31724          cuitCuil: item.cuitCuil || 'Sin CUIT', meta: [item.localidad, item.provincia].filter(Boolean).join(', ') || 'Sin ubicación',
31725        }));
31726        const estData = estList.map((e) => ({
31727          id: e.id, nombre: e.nombre, provincia: e.provincia || '?', localidad: e.localidad || '?', comitente: e.comitenteAlias || e.comitenteNombreFiscal || e.comitente || '',
31728          comitenteCuitCuil: e.comitenteCuitCuil || '', renspa: e.renspa || '',
31729          meta: [e.localidad, e.provincia].filter(Boolean).join(', ') || 'Sin ubicación',
31730          hasCoords: e.latitud != null && e.longitud != null,
31731          coords: (e.latitud != null && e.longitud != null) ?Number(e.latitud).toFixed(4) + ', ' + Number(e.longitud).toFixed(4) : '',
31732          deleting: s.estDeleting === e.id, disabled: !!s.estDeleting,
31733          remove: () => this.deleteEstablishment(e.id),
31734        }));
31735        const baForm = s.baForm || this.emptyBaForm();
31736        const buyerProfile = this.canUseBuyerData(s.apiUser) && !this.canManageSellerData(s.apiUser) && !this.isCertifierOnly(s.apiUser);
31737        const buyerDirectoryAll = (s.buyerAccounts || []).map((account) => {
31738          const base = account.displayName || account.email || 'Comprador';
31739          const initials = base.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase() || 'C';
31740          const lastLogin = account.lastLoginAt ?new Date(account.lastLoginAt) : null;
31741          return {
31742            uid: account.uid,
31743            name: account.displayName || 'Comprador',
31744            email: account.email || 'Sin email',
31745            initials,
31746            lastAccess: lastLogin && !isNaN(lastLogin) ?'Último acceso: ' + lastLogin.toLocaleDateString('es-AR') : 'Sin accesos registrados',
31747            authorize: () => this.authorizeBuyerAccount(account),
31748          };
31749        });
31750        const buyerDirectoryQuery = String(s.buyerDirectorySearch || '').trim().toLowerCase();
31751        const buyerDirectoryData = buyerDirectoryQuery
31752           ? buyerDirectoryAll.filter((account) => (account.name + ' ' + account.email).toLowerCase().includes(buyerDirectoryQuery))
31753          : buyerDirectoryAll;
31754        const todayIso = this.localIsoDate();
31755        const creditEndFor = (auth) => auth.creditEndDate || this.calculateCreditEndDate(auth.creditStartDate);
31756        const creditValidity = (auth) => {
31757          const start = auth.creditStartDate;
31758          const end = creditEndFor(auth);
31759          if (!start && !end) return 'Vigencia no informada';
31760          return 'Vigencia: ' + (start ?this.formatBaDate(start) : '—') + ' al ' + (end ?this.formatBaDate(end) : '—');
31761        };
31762        const creditIsCurrent = (auth) => {
31763          if (auth?.isActive === false) return false;
31764          const start = auth.creditStartDate;
31765          const end = creditEndFor(auth);
31766          return (!start || start <= todayIso) && (!end || end >= todayIso);
31767        };
31768        const sellerIdentity = (auth) => {
31769          const uid = String(auth?.sellerUid || '');
31770          const email = String(auth?.sellerEmail || '').trim();
31771          const resolvedSeller = String(s.sellerLabels?.[uid] || '').trim();
31772          const authorizationId = String(auth?.summaryId || auth?.id || '');
31773          const creditNotification = rawNotifications.find((notification) => {
31774            const tag = String(notification?.tag || '');
31775            const eventKey = String(notification?.eventKey || '');
31776            return authorizationId && (tag === 'roslot-credit-' + authorizationId || eventKey.startsWith(authorizationId + '|'));
31777          });
31778          const notificationBody = String(creditNotification?.body || '');
31779          const notifiedSeller = (notificationBody.match(/^(.+?)\s+(?:te autorizó|actualizó tu autorización|desactivó tu autorización)/i) || [])[1] || '';
31780          const relatedLot = (s.lots || []).find((lot) =>
31781            (uid && String(lot.sellerUid || '') === uid)
31782            || (email && String(lot.sellerEmail || '').toLowerCase() === email.toLowerCase())
31783          );
31784          const lotSeller = String(relatedLot?.seller || '').trim();
31785          const lotConsignataria = String(relatedLot?.consignatariaName || '').trim();
31786          const isGenericSeller = !lotSeller || /^productor rosgan$/i.test(lotSeller) || /^vendedor(?:\s|$)/i.test(lotSeller);
31787          const primary = String(auth?.sellerName || resolvedSeller || notifiedSeller || (!isGenericSeller ?lotSeller : '') || lotConsignataria || email || 'Vendedor autorizado').trim();
31788          const detail = email && email.toLowerCase() !== primary.toLowerCase() ?email : '';
31789          return { primary, detail };
31790        };
31791        const creditMovementView = (movement) => {
31792          const date = movement.date ?new Date(movement.date) : null;
31793          const status = String(movement.status || '').toLowerCase();
31794          return {
31795            id: movement.id,
31796            lot: movement.lot || 'Lote',
31797            date: date && !isNaN(date) ?date.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : (movement.date || 'Fecha no informada'),
31798            statusLabel: status === 'won' ?'Ganado' : status === 'winning' ?'Oferta vigente' : (status ?status.replace(/[_-]+/g, ' ').replace(/^./, (char) => char.toUpperCase()) : 'Movimiento'),
31799            statusBg: status === 'won' ?'rgba(10,92,168,.12)' : status === 'winning' ?'rgba(31,122,99,.13)' : 'rgba(101,117,138,.12)',
31800            statusColor: status === 'won' ?'#075ca8' : status === 'winning' ?'#1f7a63' : '#536a80',
31801            bidAmount: this.formatBaCurrency(movement.bidAmount),
31802            consumedAmount: this.formatBaCurrency(movement.consumedAmount),
31803          };
31804        };
31805        const inlineCreditView = (authorization) => {
31806          const key = this.creditAuthorizationKey(authorization);
31807          const summary = s.creditSummaryCache?.[key] || null;
31808          const weekly = Number(summary?.weeklyCreditAmount ?? authorization.weeklyCreditAmount) || 0;
31809          const used = Number(summary?.usedCreditAmount) || 0;
31810          const available = Number(summary?.availableCreditAmount ?? weekly) || 0;
31811          const percentRaw = weekly > 0 ?(used / weekly) * 100 : 0;
31812          const movements = (summary?.movements || []).map(creditMovementView).slice(0, 3);
31813          return {
31814            inlineLoading: !!s.creditSummaryLoadingIds?.[key] && !summary,
31815            inlineError: s.creditSummaryErrors?.[key] || '',
31816            inlineHasError: !!s.creditSummaryErrors?.[key] && !summary,
31817            inlineReady: !!summary,
31818            inlineAuthorized: this.rollEl(this.formatBaCurrency(weekly)),
31819            inlineUsed: this.rollEl(this.formatBaCurrency(used)),
31820            inlineAvailable: this.rollEl(this.formatBaCurrency(available)),
31821            inlineProgress: Math.max(0, Math.min(100, percentRaw)).toFixed(2) + '%',
31822            inlineProgressLabel: Math.round(percentRaw).toLocaleString('es-AR') + '%',
31823            inlineMovements: movements,
31824            inlineHasMovements: movements.length > 0,
31825            inlineEmpty: !!summary && movements.length === 0,
31826            refreshInline: () => this.loadCreditSummaryIntoCache(authorization, { force: true }).catch(() => {}),
31827          };
31828        };
31829        const baData = (s.buyerAuths || []).filter((a) => a.isActive && creditIsCurrent(a)).map((a) => {
31830          const buyerLabel = a.buyerName || a.buyerEmail || '(sin email)';
31831          const buyerDetail = a.buyerName && a.buyerEmail && a.buyerEmail.toLowerCase() !== a.buyerName.toLowerCase() ?a.buyerEmail : '';
31832          const initials = buyerLabel.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase() || 'C';
31833          return {
31834            id: a.id, buyer: buyerLabel, buyerDetail, hasBuyerDetail: !!buyerDetail, email: a.buyerEmail || '', uid: a.buyerUid || '',
31835            hasUid: !!a.buyerUid, initials,
31836            creditStartDate: a.creditStartDate,
31837            weeklyCreditAmount: this.moneyInputDisplay(a.weeklyCreditAmount),
31838            weeklyCreditLabel: this.formatBaCurrency(a.weeklyCreditAmount),
31839            validityLabel: creditValidity(a),
31840            isActive: a.isActive, busy: s.baActioning === a.id,
31841            statusLabel: a.isActive ?'Activo' : 'Inactivo',
31842            statusBg: a.isActive ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)',
31843            statusColor: a.isActive ?'#1f7a63' : '#c22',
31844            cardOpacity: a.isActive ?'1' : '0.62',
31845            avatarBg: a.isActive ?'linear-gradient(135deg,#0a5ca8,#1f7a63)' : 'linear-gradient(135deg,#8593a6,#647186)',
31846            knobX: a.isActive ?'20px' : '2px', switchBg: a.isActive ?'#1f7a63' : '#cdd6e2',
31847            toggle: () => this.toggleBaActive(a.id),
31848            setStartDate: (event) => this.updateBuyerAuthField(a.id, 'creditStartDate', event.target.value),
31849            setWeeklyCreditAmount: (event) => this.formatMoneyInputEvent(event, (raw) => this.updateBuyerAuthField(a.id, 'weeklyCreditAmount', raw)),
31850            save: () => this.pushBuyerAuth(a.id),
31851            saveLabel: s.baActioning === a.id ?'Guardando…' : 'Guardar',
31852            openHistory: () => this.openCreditHistory(a, 'seller'),
31853            summaryDisabled: !a.summaryId,
31854            summaryOpacity: a.summaryId ?'1' : '.45',
31855            remove: () => this.deleteBuyerAuth(a.id),
31856            ...inlineCreditView(a),
31857          };
31858        });
31859        const activeLots = mine.filter((l) => !['Caducado', 'Vendido'].includes(l.status)).length;
31860        const expiredLots = mine.length - activeLots;
31861        const offersMade = offerData.length;
31862        const wonCount = offerData.filter((o) => o.isWinner || o.standLabel === 'Vas ganando').length;
31863        const winRate = offersMade ?Math.round(wonCount / offersMade * 100) : 0;
31864        const buyerCreditRows = (s.buyerAccess || [])
31865          .filter((auth) => auth.isActive && creditIsCurrent(auth))
31866          .slice()
31867          .sort((a, b) => {
31868            const sellerCompare = String(a.sellerName || a.sellerEmail || '').localeCompare(String(b.sellerName || b.sellerEmail || ''), 'es');
31869            return sellerCompare || String(b.creditStartDate || '').localeCompare(String(a.creditStartDate || ''));
31870          })
31871          .map((a) => {
31872          const identity = sellerIdentity(a);
31873          const sellerPrimary = identity.primary;
31874          const sellerDetail = identity.detail;
31875          return {
31876            id: a.id || a.sellerUid || a.sellerEmail || a.creditStartDate,
31877            seller: sellerPrimary,
31878            sellerDetail,
31879            hasSellerDetail: !!sellerDetail,
31880            validityLabel: creditValidity(a),
31881            periodLabel: (a.creditStartDate ?this.formatBaDate(a.creditStartDate) : '—') + ' al ' + (creditEndFor(a) ?this.formatBaDate(creditEndFor(a)) : '—'),
31882            weeklyCreditLabel: this.formatBaCurrency(a.weeklyCreditAmount),
31883            statusLabel: a.isActive ?'Activo' : 'Inactivo',
31884            statusBg: a.isActive ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)',
31885            statusColor: a.isActive ?'#1f7a63' : '#c22',
31886            openHistory: () => this.openCreditHistory(a, 'buyer'),
31887            summaryDisabled: !a.summaryId,
31888            summaryOpacity: a.summaryId ?'1' : '.45',
31889            ...inlineCreditView(a),
31890            };
31891          });
31892        const creditSummary = s.creditSummary || null;
31893        const creditSummaryAuth = s.creditSummaryAuthorization || {};
31894        const creditSummaryPerspective = s.creditSummaryPerspective === 'seller' ?'seller' : 'buyer';
31895        const creditSummaryWeekly = Number(creditSummary?.weeklyCreditAmount) || 0;
31896        const creditSummaryUsed = Number(creditSummary?.usedCreditAmount) || 0;
31897        const creditSummaryAvailable = Number(creditSummary?.availableCreditAmount) || 0;
31898        const creditSummaryPercentRaw = creditSummaryWeekly > 0 ?(creditSummaryUsed / creditSummaryWeekly) * 100 : 0;
31899        const creditSummaryPercent = Math.max(0, Math.min(100, creditSummaryPercentRaw));
31900        const creditMovementRows = (creditSummary?.movements || []).map(creditMovementView);
31901        const creditSummaryParty = creditSummaryPerspective === 'seller'
31902           ? (creditSummaryAuth.buyerName || creditSummaryAuth.buyerEmail || 'Comprador')
31903          : (creditSummaryAuth.sellerName || creditSummaryAuth.sellerEmail || 'Vendedor');
31904        const creditSummaryPeriod = (creditSummaryAuth.creditStartDate ?this.formatBaDate(creditSummaryAuth.creditStartDate) : '—') + ' al ' + (creditEndFor(creditSummaryAuth) ?this.formatBaDate(creditEndFor(creditSummaryAuth)) : '—');
31905        const creditHistoryPerspective = s.creditHistoryPerspective === 'seller' ?'seller' : 'buyer';
31906        const creditHistoryAnchor = s.creditHistoryAuthorization || null;
31907        const creditHistorySource = creditHistoryPerspective === 'seller' ?(s.buyerAuths || []) : (s.buyerAccess || []);
31908        const sameCreditParty = (authorization) => {
31909          if (!creditHistoryAnchor) return true;
31910          if (creditHistoryPerspective === 'seller') {
31911            const anchorUid = String(creditHistoryAnchor.buyerUid || '');
31912            const anchorEmail = String(creditHistoryAnchor.buyerEmail || '').toLowerCase();
31913            return (anchorUid && String(authorization.buyerUid || '') === anchorUid)
31914              || (anchorEmail && String(authorization.buyerEmail || '').toLowerCase() === anchorEmail);
31915          }
31916          const anchorUid = String(creditHistoryAnchor.sellerUid || '');
31917          const anchorEmail = String(creditHistoryAnchor.sellerEmail || '').toLowerCase();
31918          return (anchorUid && String(authorization.sellerUid || '') === anchorUid)
31919            || (anchorEmail && String(authorization.sellerEmail || '').toLowerCase() === anchorEmail);
31920        };
31921        const creditHistoryRows = creditHistorySource
31922          .filter((authorization) => sameCreditParty(authorization) && !creditIsCurrent(authorization))
31923          .slice()
31924          .sort((a, b) => String(b.creditStartDate || '').localeCompare(String(a.creditStartDate || '')))
31925          .map((authorization) => {
31926            const party = creditHistoryPerspective === 'seller'
31927               ? (authorization.buyerName || authorization.buyerEmail || 'Comprador')
31928              : sellerIdentity(authorization).primary;
31929            const isInactive = authorization.isActive === false;
31930            return {
31931              id: this.creditAuthorizationKey(authorization) || authorization.creditStartDate,
31932              party,
31933              period: (authorization.creditStartDate ?this.formatBaDate(authorization.creditStartDate) : '—') + ' al ' + (creditEndFor(authorization) ?this.formatBaDate(creditEndFor(authorization)) : '—'),
31934              amount: this.formatBaCurrency(authorization.weeklyCreditAmount),
31935              statusLabel: isInactive ?'Inactivo' : 'Finalizado',
31936              statusBg: isInactive ?'rgba(101,117,138,.12)' : 'rgba(10,92,168,.11)',
31937              statusColor: isInactive ?'#596b7e' : '#075ca8',
31938              openDetail: () => this.openCreditSummary(authorization, creditHistoryPerspective),
31939              detailDisabled: !this.creditAuthorizationKey(authorization),
31940              detailOpacity: this.creditAuthorizationKey(authorization) ?'1' : '.45',
31941            };
31942          });
31943        const creditHistoryParty = creditHistoryAnchor
31944           ? (creditHistoryPerspective === 'seller'
31945              ? (creditHistoryAnchor.buyerName || creditHistoryAnchor.buyerEmail || 'Comprador')
31946              : sellerIdentity(creditHistoryAnchor).primary)
31947          : 'Todos los créditos anteriores';
31948        const winningOffers = liveOffers.filter((offer) => offer.winning).length;
31949        const outbidOffers = liveOffers.filter((offer) => offer.outbid).length;
31950        const adjudicatedAmount = completedOffers.reduce((sum, offer) => sum + (Number(offer.amountNum) || 0), 0);
31951        const receivedOffers = mine.reduce((sum, lot) => sum + (Number(lot.bidCount) || (lot.bids || []).length || 0), 0);
31952        const soldLots = mine.filter((lot) => lot.auctionStatus === 'sold');
31953        const closedLots = mine.filter((lot) => ['sold', 'closed_no_bids'].includes(lot.auctionStatus));
31954        const closedWithoutBids = mine.filter((lot) => lot.auctionStatus === 'closed_no_bids').length;
31955        const sellerClosedSummary = [soldLots.length ?soldLots.length + ' vendidos' : '', closedWithoutBids ?closedWithoutBids + ' sin ofertas' : ''].filter(
31955Boolean).join(' · ') || 'al día';
31956        const salesConversion = closedLots.length ?Math.round(soldLots.length / closedLots.length * 100) : 0;
31957        const series = (values) => {
31958          const source = values.length >= 2 ?values : values.length === 1 ?[0, values[0]] : [0, 0];
31959          return this.linePath(source.map((value) => value * P), 120, 34, 3);
31960        };
31961        const cumulative = (values) => { let total = 0; return values.map((value) => (total += value)); };
31962        let offersSeen = 0, winsSeen = 0;
31963        const winProgress = offerData.map((offer) => {
31964          offersSeen += 1;
31965          if (offer.isWinner || offer.standLabel === 'Vas ganando') winsSeen += 1;
31966          return winsSeen / offersSeen * 100;
31967        });
31968        const sellerKpis = [
31969          { label: 'Lotes activos', value: fmtK(activeLots * P), delta: sellerClosedSummary, color: '#0a5ca8', spark: series(cumulative(mine.map((l) => !['Caducado', 'Vendido'].includes(l.status) ?1 : 0))) },
31970          { label: 'Cabezas publicadas', value: fmtK(totalCabezas * P), delta: mine.length + ' lotes', color: '#1f7a63', spark: series(cumulative(mine.map((l) => Number(l.cabezas) || 0))) },
31971          { label: 'Ofertas recibidas', value: fmtK(receivedOffers * P), delta: activeLots + ' lotes activos', color: '#e63946', spark: series(cumulative(mine.map((lot) => Number(lot.bidCount) || 0))) },
31972          { label: 'Lotes vendidos', value: fmtK(soldLots.length * P), delta: soldLots.length + '/' + closedLots.length + ' cerrados', color: '#8a5a2b', spark: series(cumulative(mine.map((lot) => lot.auctionStatus === 'sold' ?1 : 0))) },
31973          { label: 'Conversión de venta', value: Math.round(salesConversion * P) + '%', delta: closedLots.length ?soldLots.length + '/' + closedLots.length : 'sin cierres', color: '#5b3fa8', spark: series(cumulative(closedLots.map((lot) => lot.auctionStatus === 'sold' ?1 : 0))) },
31974        ];
31975        const buyerKpis = [
31976          { label: 'Ofertas activas', value: fmtK(liveOffers.length * P), delta: offersMade + ' realizadas', color: '#0a5ca8', spark: series(cumulative(offerData.map(() => 1))) },
31977          { label: 'Vas ganando', value: fmtK(winningOffers * P), delta: liveOffers.length ?winningOffers + '/' + liveOffers.length + ' activas' : 'sin subastas abiertas', color: '#1f7a63', spark: series(cumulative(liveOffers.map((offer) => offer.winning ?1 : 0))) },
31978          { label: 'No liderás', value: fmtK(outbidOffers * P), delta: outbidOffers ?'requieren atención' : 'sin alertas', color: '#e63946', spark: series(cumulative(liveOffers.map((offer) => offer.outbid ?1 : 0))) },
31979          { label: 'Lotes ganados', value: fmtK(completedOffers.length * P), delta: completedOffers.length ?'ventas cerradas' : 'sin adjudicaciones', color: '#5b3fa8', spark: series(cumulative(completedOffers.map(() => 1))) },
31980          { label: 'Monto adjudicado', value: this.fmtARS(adjudicatedAmount * P), delta: completedOffers.length + ' lotes', color: '#8a5a2b', spark: series(cumulative(completedOffers.map((offer) => Number(offer.amountNum) || 0))) },
31981        ];
31982        const kpis = (buyerProfile ?buyerKpis : sellerKpis).map((k) => ({ ...k, deltaBg: 'rgba(101,117,138,0.12)', deltaColor: '#65758a' }));
31983        const myLotBidCounts = mine.map((lot) => Number(lot.bidCount) || (lot.bids || []).length || 0);
31984        const rawActivityPoints = buyerProfile ?offerData.map(() => 1) : myLotBidCounts;
31985        const activityPoints = rawActivityPoints.length >= 2 ?rawActivityPoints : rawActivityPoints.length === 1 ?[0, rawActivityPoints[0]] : [0, 0];
31986        const mySeries = cumulative(activityPoints).map((v) => v * P);
31987        const marketOffersTotal = all.reduce((sum, lot) => sum + (Number(lot.bidCount) || (lot.bids || []).length || 0), 0);
31988        const marketPace = all.length ?marketOffersTotal / all.length : 0;
31989        const mktSeries = mySeries.map((_, index) => marketPace * (index + 1) * P);
31990        const scaleMax = Math.max(...mySeries, ...mktSeries, 1);
31991        const norm = (arr) => arr.map((v) => (v / scaleMax) * 9);
31992        const distributionItems = buyerProfile
31993           ? [...new Set(offerData.map((offer) =>
31993 offer.categoria).filter(Boolean))].map((categoria) => ({
31994              label: categoria,
31995              count: offerData.filter((offer) => offer.categoria === categoria).length,
31996            }))
31997          : [...new Set(mine.map((lot) => lot.categoria).filter(Boolean))].map((categoria) => ({
31998              label: categoria,
31999              count: mine.filter((lot) => lot.categoria === categoria).reduce((sum, lot) => sum + (Number(lot.cabezas) || 0), 0),
32000            }));
32001        const dColors = ['#0a5ca8', '#1f7a63', '#e63946', '#5b3fa8', '#d18a1d', '#27879b'];
32002        const dCounts = distributionItems.map((item) => item.count);
32003        const distributionTotal = dCounts.reduce((a, b) => a + b, 0);
32004        const dTot = distributionTotal || 1;
32005        let acc = 0;
32006        const stops = dCounts.map((c, i) => { const from = acc / dTot * 360; acc += c; const to = acc / dTot * 360; return `${dColors[i % dColors.length]} ${from.toFixed(1)}deg ${to.toFixed(1)}deg`; });
32007        const isKgLot = (l) => /kg/i.test(l.condicion || '') && Number(l.baseNum) > 0 && Number(l.baseNum) < 100000;
32008        const average = (items) => items.length ?items.reduce((sum, item) => sum + Number(item.baseNum), 0) / items.length : 0;
32009        const priceCats = [['Novillo', /novill/i], ['Ternero', /tern/i], ['Vaca', /vaca/i], ['Vaquillona', /vaquill/i], ['Toro', /toro/i]].map(([label, pattern]) => {
32010          const market = average(all.filter((l) => isKgLot(l) && pattern.test(l.categoria)));
32011          const own = buyerProfile
32012            ?average(offerData.filter((offer) => pattern.test(offer.categoria)).map((offer) => ({ baseNum: offer.amountNum })))
32013            : average(mine.filter((l) => isKgLot(l) && pattern.test(l.categoria)));
32014          return [label, Math.round(own || market), Math.round(market)];
32015        }).filter((entry) => entry[2] > 0).slice(0, 5);
32016        const pMax = Math.max(...priceCats.flatMap((c) => [c[1], c[2]]), 1);
32017        const stateMap = (st) => st === 'En vivo' ?{ state: 'EN VIVO', stateBg: '#e63946', stateColor: '#fff' } : st === 'Próximo' ?{ state: 'PROGRAMADO', stateBg: '#0a5ca8', stateColor: '#fff' } : st === 'Caducado' ?{ state: 'CADUCADO', stateBg: 'rgba(101,117,138,0.92)', stateColor: '#fff' } : st === 'Listo para la carga' ?{ state: 'LISTO PARA LA CARGA', stateBg: '#146b68', stateColor: '#fff' } : st === 'Vendido' ?{ state: 'VENDIDO', stateBg: '#1f7a63', stateColor: '#fff' } : { state: 'PUBLICADO', stateBg: 'rgba(0,29,61,0.82)', stateColor: '#fff' };
32018        const mineSorted = [...mine].sort((a, b) => (['Caducado', 'Vendido'].includes(a.status) ?-1 : 0) - (['Caducado', 'Vendido'].includes(b.status) ?-1 : 0));
32019        const republishForm = s.republishForm || {};
32020        const canManageBuyers = this.canManageBuyerAccounts(s.apiUser);
32021        const canManageAdmin = this.canManageUsers(s.apiUser);
32022        const canSell = this.canManageSellerData(s.apiUser);
32023        const canBuy = this.canUseBuyerData(s.apiUser);
32024        const launchAdminRows = (Array.isArray(s.launchInterestsAdmin) ?s.launchInterestsAdmin : []).map((interest) => {
32025          const dateValue = interest.updatedAt || interest.createdAt || '';
32026          const date = dateValue ?new Date(dateValue) : null;
32027          return {
32028            name: interest.displayName || interest.email || 'Usuario Rosgan',
32029            email: interest.email || 'Sin email',
32030            phone: interest.phoneNumber || 'Sin teléfono',
32031            emailOptIn: interest.emailOptIn !== false,
32032            whatsappOptIn: interest.whatsappOptIn === true,
32033            date: date && !isNaN(date) ?'Anotado el ' + date.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'Fecha no disponible',
32034          };
32035        });
32036        const lotYieldRows = (Array.isArray(s.lotYieldCategories) ?s.lotYieldCategories : []).map((category) => {
32037          const saving = String(s.lotYieldSavingId || '') === String(category.id);
32038          return {
32039            id: category.id,
32040            name: category.name || category.id,
32041            code: category.code || 'SIN CÓDIGO',
32042            percent: category.uiPercent ?? '',
32043            saving,
32044            saveLabel: saving ?'Guardando…' : 'Guardar',
32045            saveOpacity: saving ?'.55' : '1',
32046            setPercent: (event) => this.setLotYieldPercent(category.id, event.target.value),
32047            save: () => this.saveLotYieldCategory(category.id),
32048          };
32049        });
32050        const profileErrors = [s.authSyncError, s.apiSessionError, s.myLotsError, s.myBidsError, s.pendingBidsError, s.establishmentsError, s.buyerAccountsError, s.buyerAuthsError, s.buyerAccessError].filter(
32050Boolean);
32051        const fetchInfo = s.profileFetchInfo || {};
32052        const allProfileEmpty = !mine.length && !offerData.length && !pendData.length && !estData.length && !buyerDirectoryData.length && !baData.length && !buyerCreditRows.length && !s.myLotsLoading && !s.myBidsLoading && !s.pendingBidsLoading && !s.establishmentsLoading && !s.buyerAccountsLoading && !s.buyerAuthsLoading && !s.buyerAccessLoading;
32053        const fetchInfoText = ['lotes', 'establecimientos', 'ofertas', 'pendientes', 'cuentas comprador', 'autorizaciones', 'mi crédito']
32054          .map((key) => {
32055            const info = fetchInfo[key] || {};
32056            const value = info.error ?info.error : (info.count == null ?'sin respuesta' : String(info.count));
32057            return key + ': ' + value;
32058          })
32059          .join(' · ');
32060        const profileEmptyDebug = allProfileEmpty
32061          ?'No hay datos para esta sesión. Email: ' + (s.user?.email || 'sin email') + ' · UID: ' + (s.user?.uid || 'sin uid') + ' · API: ' + fetchInfoText
32062          : '';
32063        const profileConsignatariaRef = s.userConsignataria?.id || s.userConsignataria?.slug || s.userConsignataria?.ref || s.userConsignataria?.name || '';
32064        const profileConsignatariaLogo = this.consLogo(profileConsignatariaRef);
32065        const profileConsignatariaBadges = (s.userConsignatarias || []).map((item) => {
32066          const ref = item.id || item.slug || item.ref || item.name || '';
32067          const match = (s.consignatarias || []).find((consignataria) => [consignataria.id, consignataria.slug, consignataria.name, consignataria.nombre].some((value) => this.consKey(value) === this.consKey(ref)));
32068          const originalIcon = this.consLogo(ref);
32069          const whiteIcon = this.consWhiteLogo(ref);
32070          const icon = whiteIcon || originalIcon;
32071          const brandColor = this.consBrandColor(ref);
32072          return icon ? {
32073            label: match?.name || match?.nombre || item.name || ref,
32074            icon,
32075            iconTitle: match?.name || match?.nombre || item.name || ref,
32076            iconPadding: '0',
32077            iconRadius: '0',
32078            iconBg: 'transparent',
32079            iconFilter: whiteIcon ?'none' : 'brightness(0) invert(1)',
32080            badgeBg: brandColor,
32081            badgeBorder: 'rgba(255,255,255,0.42)',
32082            badgeColor: '#fff',
32083          } : null;
32084        }).filter(Boolean);
32085        const lucideRole = (label, icon) => ({ label, icon, iconTitle: '', iconPadding: '0', iconRadius: '0', iconBg: 'transparent', iconFilter: 'brightness(0) invert(1)', badgeBg: 'rgba(255,255,255,0.14)', badgeBorder: 'rgba(255,255,255,0.22)', badgeColor: '#fff' });
32086        const roleMeta = {
32087          user: lucideRole('Usuario', 'assets/icons/lucide/user-round-check.svg'),
32088          buyer: lucideRole('Comprador', 'assets/icons/lucide/shopping-cart.svg'),
32089          comprador: lucideRole('Comprador', 'assets/icons/lucide/shopping-cart.svg'),
32090          seller: lucideRole('Vendedor', 'assets/icons/lucide/store.svg'),
32091          vendedor: lucideRole('Vendedor', 'assets/icons/lucide/store.svg'),
32092          consignatario: lucideRole('Consignatario', 'assets/icons/lucide/landmark.svg'),
32093          certificador: lucideRole('Certificador', 'assets/icons/lucide/badge-check.svg'),
32094          admin: lucideRole('Administrador', 'assets/icons/lucide/shield-check.svg'),
32095          sysadmin: lucideRole('Administrador del sistema', 'assets/icons/lucide/settings.svg'),
32096          dev: lucideRole('Dev', 'assets/icons/lucide/code.svg'),
32097        };
32098        const rawProfileRoles = this.apiRoleIds(s.apiUser);
32099        const inferredProfileRoles = rawProfileRoles.length ?rawProfileRoles : [...(canSell ?['seller'] : []), ...(canBuy ?['buyer'] : [])];
32100        const profileRoleBadges = inferredProfileRoles
32101          .map((role) => roleMeta[role] || lucideRole(String(role).replace(/(^|\s)\S/g, (letter) => letter.toUpperCase()), 'assets/icons/lucide/user-round-check.svg'))
32102          .filter((role, index, roles) => roles.findIndex((candidate) => candidate.label === role.label) === index)
32103          .concat(profileConsignatariaBadges);
32104        const profileEditorForm = s.profileEditorForm || {};
32105        const profileEditorAvatar = s.profilePhotoPreview || currentProfilePhoto || profileConsignatariaLogo || '';
32106        const adminRoleDefs = [
32107          ['user', 'Usuario', 'user-round-check.svg'],
32108          ['buyer', 'Comprador', 'shopping-cart.svg'],
32109          ['seller', 'Vendedor', 'store.svg'],
32110          ['consignatario', 'Consignatario', 'landmark.svg'],
32111          ['certificador', 'Certificador', 'badge-check.svg'],
32112          ['admin', 'Administrador', 'shield-check.svg'],
32113          ['sysadmin', 'Administrador del sistema', 'settings.svg'],
32114          ['dev', 'Dev', 'code.svg'],
32115        ];
32116        const adminRoleLabel = (role) => adminRoleDefs.find((item) => item[0] === role)?.[1] || role;
32117        const directory = this.adminUserDirectory();
32118        const directoryCompany = (s.adminConsignatarias || []).find(c =>
32118 String(c.id) === String(s.adminUsersConsignataria));
32119        const directoryCompanyLogo = directoryCompany ? (this.consLogo(directoryCompany.name || directoryCompany.nombre || directoryCompany.id) || directoryCompany.logoUrl || directoryCompany.logo || '') : '';
32120        const adminUsersList = directory.rows.map((account) => {
32121          const active = s.adminSelectedUser?.uid === account.uid;
32122          return {
32123            ...account,
32124            versionDevices: s.adminVersionsAvailable === false ? [{label:'No se pudo consultar la versión',detail:'Volvé a actualizar el listado',color:'#64748b',bg:'#f1f5f9'}] : window.RosganAppUpdate.describeDevices(account.appDevices, s.adminCurrentVersion),
32125            initials: (account.displayName || account.email || 'U').split(/[\s@.]+/).slice(0, 2).map((word) => word[0]?.toUpperCase() || '').join(''),
32126            hasPhoto: !!account.photoUrl,
32127            showsInitials: !account.photoUrl,
32128            roleSummary: (account.roles || []).map(adminRoleLabel).join(' · ') || 'Sin rol',
32129            companyBadges: account.roles.some(role => ['consignatario','certificador'].includes(role)) ? [...new Set(account.consignatariaIds || [])].map(id => {
32130              const company = (s.adminConsignatarias || []).find(c => String(c.id) === String(id));
32131              const name = company?.name || company?.nombre || 'Consignataria no disponible';
32132              const ref = company?.name || company?.nombre || id;
32133              const logo = this.consLogo(ref) || company?.logoUrl || company?.logo || '';
32134              return {name,logo,hasLogo:!!logo,color:this.consBrandColor(ref)};
32135            }) : [],
32136            roleBadges: (account.roles?.length ? account.roles : ['none']).map(role => ({label:role === 'none' ? 'Sin rol' : adminRoleLabel(role),icon:'assets/icons/lucide/' + (adminRoleDefs.find(def => def[0] === role)?.[2] || 'user-round-check.svg'),bg:['consignatario','certificador'].includes(role)?'#e7f5f1':'#edf3fa',color:['consignatario','certificador'].includes(role)?'#087561':'#174e7e'})),
32137            border: active ?'#0a5ca8' : '#dce6ef',
32138            bg: active ?'#edf6fd' : '#fff',
32139            select: () => this.selectAdminUser(account),
32140          };
32141        });
32142        const adminSelectedRoles = s.adminSelectedRoles || [];
32143        const adminRoleOptions = adminRoleDefs.map(([id, label, icon]) => {
32144          const active = adminSelectedRoles.includes(id);
32145          return {
32146            id, label,
32147            icon: 'assets/icons/lucide/' + icon,
32148            bg: active ?'#003b72' : '#f5f8fb',
32149            border: active ?'#003b72' : '#d6e1eb',
32150            color: active ?'#fff' : '#40536a',
32151            iconFilter: active ?'brightness(0) invert(1)' : 'none',
32152            toggle: () => this.toggleAdminRole(id),
32153          };
32154        });
32155        const selectedConsignatariaIds = new Set(s.adminSelectedConsignatariaIds || []);
32156        const adminConsignatariaOptions = (s.adminConsignatarias || []).map((cons) => {
32157          const id = String(cons.id);
32158          const active = selectedConsignatariaIds.has(id);
32159          return {
32160            id,
32161            selected: active ?'true' : 'false',
32162            name: cons.name || cons.nombre || id,
32163            logo: this.consLogo(id) || cons.logoUrl || cons.logo,
32164            border: active ?'#0a5ca8' : '#d9e4ee',
32165            bg: active ?'#eaf5fd' : '#fff',
32166            select: () => this.selectAdminConsignataria(id),
32167          };
32168        });
32169        const adminUserMessage = s.adminUserMessage || null;
32170        const mailingStatus = s.mailingStatus || {};
32171        const mailingSelected = (s.mailingCampaigns || []).find((campaign) => campaign.id === s.mailingSelectedId) || null;
32172        const mailingDraft = s.mailingDraft || {};
32173        const mailingStatusLabels = { draft: 'Guardada', queued: 'En cola', sending: 'Enviando', completed: 'Completada', failed: 'Con error', cancelled: 'Cancelada' };
32174        const mailingCampaignList = (s.mailingCampaigns || []).map((campaign) => {
32175          const active = campaign.id === s.mailingSelectedId;
32176          const total = Number(campaign.total) || 0;
32177          const processed = (Number(campaign.sent) || 0) + (Number(campaign.failed) || 0);
32178          const sentTotal = (Number(campaign.sent) || 0) + (Number(campaign.testCount) || 0);
32179          const lastSendAt = campaign.completedAt || campaign.lastTestAt || campaign.startedAt || campaign.queuedAt || '';
32180          const deleteDisabled = !!s.mailingBusy || ['queued', 'sending'].includes(campaign.status);
32181          return {
32182            ...campaign,
32183            name: campaign.name || campaign.subject || 'Sin nombre',
32184            statusLabel: mailingStatusLabels[campaign.status] || campaign.status || 'Guardada',
32185            audienceLabel: campaign.audience === 'launch_interest' ?'Lanzamiento' : 'Base general',
32186            progress: total ?processed + '/' + total : '',
32187            sendMeta: sentTotal ?sentTotal.toLocaleString('es-AR') + ' enviado' + (sentTotal === 1 ?'' : 's') + ' · ' + this.mailingReportDate(lastSendAt) : 'Sin envíos',
32188            border: active ?'#0a5ca8' : '#dce6ef', bg: active ?'#edf6fd' : '#fff',
32189            select: () => this.selectMailingCampaign(campaign),
32190            remove: () => this.deleteMailingCampaign(campaign),
32191            deleteDisabled,
32192            deleteOpacity: deleteDisabled ?'.35' : '1',
32193          };
32194        });
32195        const mailingMessage = s.mailingMessage || null;
32196        const mailingLocked = !!mailingSelected && mailingSelected.status !== 'draft';
32197        const mailingInsertType = s.mailingInsertDialog || '';
32198        const mailingInsertIsImage = mailingInsertType === 'image';
32199        const mailingInsertUrlValid = !!this.mailingNormalizeUrl(s.mailingInsertUrl || '');
32200        const mailingImageSize = ['small', 'medium', 'large', 'full'].includes(s.mailingInsertImageSize) ?s.mailingInsertImageSize : 'medium';
32201        const mailingImageSizeWidths = { small: '320px', medium: '480px', large: '640px', full: '100%' };
32202        const mailingImageLinkRaw = String(s.mailingInsertImageLink || '').trim();
32203        const mailingImageLinkValid = !mailingImageLinkRaw || /^https?:/i.test(this.mailingNormalizeUrl(mailingImageLinkRaw));
32204        const mailingReport = s.mailingReport || null;
32205        const mailingReportSummary = mailingReport?.summary || {};
32206        const mailingReportNumber = (value) => (Number(value) || 0).toLocaleString('es-AR');
32207        const mailingReportRate = (value) => (Number(value) || 0).toLocaleString('es-AR', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + '%';
32208        const mailingReportStatusMeta = {
32209          accepted: ['Aceptado', '#eaf3fb', '#276087'], delivered: ['Entregado', '#e7f6f0', '#17664f'],
32210          opened: ['Abierto', '#e8f2ff', '#075ca8'], clicked: ['Clic', '#eeeafd', '#5b3fa8'],
32211          temporary_fail: ['Rebote temporal', '#fff6df', '#946200'], permanent_fail: ['Rebote definitivo', '#fff0f1', '#b42335'],
32212          unsubscribed: ['Desuscripto', '#f1f3f5', '#586575'], complained: ['Queja', '#fff0f1', '#9e2637'],
32213        };
32214        const mailingReportRowsAll = (mailingReport?.deliveries || []).map((delivery) => {
32215          const meta = mailingReportStatusMeta[delivery.status] || [delivery.status || 'Sin estado', '#f1f3f5', '#586575'];
32216          const isTest = delivery.kind === 'test' || delivery.test === true;
32217          return {
32218            ...delivery,
32219            name: delivery.name || '',
32220            kindLabel: isTest ?'Prueba' : 'Campaña', kindBg: isTest ?'#fff4df' : '#eaf3fb', kindColor: isTest ?'#8b5d00' : '#276087',
32221            statusLabel: meta[0], statusBg: meta[1], statusColor: meta[2],
32222            sentAt: this.mailingReportDate(delivery.sentAt), deliveredAt: this.mailingReportDate(delivery.deliveredAt),
32223            openedAt: this.mailingReportDate(delivery.lastOpenedAt || delivery.openedAt), clickedAt: this.mailingReportDate(delivery.lastClickedAt || delivery.clickedAt),
32224            openCount: Number(delivery.openCount) || 0, clickCount: Number(delivery.clickCount) || 0,
32225            error: delivery.error || '—',
32226          };
32227        }).sort((a, b) => String(a.email || '').localeCompare(String(b.email || ''), 'es', { sensitivity: 'base' }));
32228        const mailingReportSearch = String(s.mailingReportSearch || '').trim().toLocaleLowerCase('es');
32229        const mailingReportStatusFilter = String(s.mailingReportStatusFilter || 'all');
32230        const mailingReportLetter = String(s.mailingReportLetter || 'all').toUpperCase();
32231        const mailingReportEmailLetter = (email) => {
32232          const first = String(email || '').trim().charAt(0).toUpperCase();
32233          return /^[A-Z]$/.test(first) ?first : '#';
32234        };
32235        const mailingReportBaseRows = mailingReportRowsAll.filter((row) => {
32236          const matchesSearch = !mailingReportSearch
32237            || String(row.email || '').toLocaleLowerCase('es').includes(mailingReportSearch)
32238            || String(row.name || '').toLocaleLowerCase('es').includes(mailingReportSearch);
32239          const matchesStatus = mailingReportStatusFilter === 'all' || row.status === mailingReportStatusFilter;
32240          return matchesSearch && matchesStatus;
32241        });
32242        const mailingReportFilteredRows = mailingReportBaseRows.filter((row) => mailingReportLetter === 'ALL' || mailingReportEmailLetter(row.email) === mailingReportLetter);
32243        const mailingReportPageSize = 50;
32244        const mailingReportPageCount = Math.max(1, Math.ceil(mailingReportFilteredRows.length / mailingReportPageSize));
32245        const mailingReportPage = Math.min(mailingReportPageCount, Math.max(1, Number(s.mailingReportPage) || 1));
32246        const mailingReportPageStart = (mailingReportPage - 1) * mailingReportPageSize;
32247        const mailingReportRows = mailingReportFilteredRows.slice(mailingReportPageStart, mailingReportPageStart + mailingReportPageSize);
32248        const mailingReportLetters = ['ALL', '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'].map((letter) => {
32249          const count = letter === 'ALL' ?mailingReportBaseRows.length : mailingReportBaseRows.filter((row) => mailingReportEmailLetter(row.email) === letter).length;
32250          const active = mailingReportLetter === letter;
32251          return {
32252            letter,
32253            label: letter === 'ALL' ?'Todos' : letter,
32254            count,
32255            disabled: count === 0,
32256            bg: active ?'#075ca8' : '#fff',
32257            color: active ?'#fff' : count ?'#355069' : '#a6b2bd',
32258            border: active ?'#075ca8' : '#d6e2ec',
32259            opacity: count ?'1' : '.45',
32260            select: () => { if (count) this.setState({ mailingReportLetter: letter.toLowerCase(), mailingReportPage: 1 }); },
32261          };
32262        });
32263        const mailingReportKpis = [
32264          ['ENVIADOS', mailingReportSummary.sent, mailingReportNumber(mailingReportSummary.campaignAccepted) + ' campaña · ' + mailingReportNumber(mailingReportSummary.tests) + ' prueba' + ((Number(mailingReportSummary.tests) || 0) === 1 ?'' : 's'), '#eef5fb', '#cbddea', '#48677f'],
32265          ['ENTREGADOS', mailingReportSummary.delivered, mailingReportRate(mailingReportSummary.deliveryRate) + ' de entrega', '#edf9f5', '#b9e3d4', '#17664f'],
32266          ['APERTURAS ÚNICAS', mailingReportSummary.opened, mailingReportRate(mailingReportSummary.openRate) + ' de apertura', '#edf6ff', '#c6dcf2', '#075ca8'],
32267          ['CLICS ÚNICOS', mailingReportSummary.clicked, mailingReportRate(mailingReportSummary.clickRate) + ' de clics', '#f2effd', '#d9cef5', '#5b3fa8'],
32268          ['REBOTES', mailingReportSummary.bounced, mailingReportNumber(mailingReportSummary.permanentFailed) + ' definitivos · ' + mailingReportNumber(mailingReportSummary.temporaryFailed) + ' temporales', '#fff8e8', '#eed9a4', '#8b6300'],
32269          ['BAJAS / QUEJAS', (Number(mailingReportSummary.unsubscribed) || 0) + (Number(mailingReportSummary.complained) || 0), mailingReportNumber(mailingReportSummary.unsubscribed) + ' bajas · ' + mailingReportNumber(mailingReportSummary.complained) + ' quejas', '#fff1f2', '#efc9ce', '#9e2637'],
32270        ].map(([label, value, detail, bg, border, color]) => ({ label, value: mailingReportNumber(value), detail, bg, border, color }));
32271        const mailingReportRates = [
32272          ['entrega', mailingReportSummary.deliveryRate], ['apertura', mailingReportSummary.openRate],
32273          ['clics', mailingReportSummary.clickRate], ['rebotes', mailingReportSummary.bounceRate], ['bajas', mailingReportSummary.unsubscribeRate],
32274        ].map(([label, value]) => ({ label, value: mailingReportRate(value) }));
32275        const mailingProductionReady = mailingStatus.provider === 'mailgun'
32276          && mailingStatus.domainState === 'active'
32277          && mailingStatus.sendingEnabled !== false;
32278        return {
32279          profileRoslotVisible: !this.isRosganProfile(),
32280          profileRosganVisible: this.isRosganProfile(),
32281          profileFeriasVisible: this.canViewProfileFerias(),
32282          profileFeriasTitle: this.canManageUsers(s.apiUser) ? 'Remates feria de todas las consignatarias' : 'Remates feria de tu consignataria',
32283          profileFeriaRows: this.profileFeriaRows(),
32284          profileFeriasLoading: !!s.profileFeriasLoading, feriaDeleting: !!s.feriaDeleting,
32285          profileFeriasError: s.profileFeriasError || '',
32286          profileFeriasEmpty: !s.profileFeriasLoading && !s.profileFeriasError && !this.profileFeriaRows().length,
32287          refreshProfileFerias: () => this.fetchProfileFerias(),
32288          profileShowRosganLots: this.isRosganProfile() && s.profileTab === 'resumen',
32289          profileKicker: this.isBasicUserProfile(s.apiUser) ? 'MI PERFIL' : this.isRosganProfile() ? 'ROSGAN · MI PANEL' : 'PANEL DE MERCADO · MI ACTIVIDAD',
32290          profileCompanyName: profileConsignatariaBadges.map((item) => item.label).join(' · ') || (s.userConsignataria ? s.userConsignataria.name : 'Sin consignataria asignada'),
32291          profileCompanyLogo: profileConsignatariaLogo,
32292          profileCompanyColor: this.consBrandColor(profileConsignatariaRef) || '#002e5f',
32293          profileRosganRows: this.profileRosganRows(),
32294          profileRosganLoading: !!s.profileRosganLoading,
32295          profileRosganError: s.profileRosganError || '',
32296          profileRosganEmpty: !s.profileRosganLoading && !s.profileRosganError && !(s.profileRosganLots || []).length,
32297          profileRosganEmptyText: (s.userConsignatarias || []).length ? 'No hay lotes de tu consignataria en próximos remates.' : 'Tu cuenta todavía no tiene una consignataria asignada.',
32298          refreshProfileRosgan: () => this.fetchProfileRosganLots(),
32299          profileName: currentProfileName || s.user?.displayName || (s.user?.email || '').split('@')[0] || 'Usuario Rosgan',
32300          profileEmptyDebug: this.isRosganProfile() ? '' : profileEmptyDebug,
32301          profileEmail: s.user?.email || '',
32302          profileRoleBadges,
32303          profileCanSell: canSell || this.isRosganProfile(),
32304          goLoad: () => this.isRosganProfile() ? this.go('rosganLotLoad') : this.beginNewLot(),
32305          profileCanManageUsers: this.canManageUsers(s.apiUser),
32306          mailingOpen: !s.publicAdvanceMode && s.route === 'mailingAdmin' && this.canManageUsers(s.apiUser),
32307          mailingLoading: !!s.mailingLoading,
32308          mailingBusy: !!s.mailingBusy,
32309          mailingBusyOpacity: s.mailingBusy ?'.55' : '1',
32310          mailingHasError: !!s.mailingError,
32311          mailingError: s.mailingError || '',
32312          mailingHasMessage: !!mailingMessage,
32313          mailingMessageText: mailingMessage?.text || '',
32314          mailingMessageBg: mailingMessage?.ok ?'#edf9f5' : '#fff0f1',
32315          mailingMessageBorder: mailingMessage?.ok ?'#b9e3d4' : '#f2c7cc',
32316          mailingMessageColor: mailingMessage?.ok ?'#17664f' : '#b42335',
32317          mailingStatusLabel: mailingProductionReady ?'Mailgun operativo' : (s.mailingStatus ?'Mailgun pendiente' : 'Mailgun sin consultar'),
32318          mailingStatusBg: mailingProductionReady ?'rgba(29,173,121,.35)' : 'rgba(238,166,35,.35)',
32319          mailingGridCols: s.isMobile ?'1fr' : 'minmax(250px,.72fr) minmax(0,1.28fr)',
32320          mailingContactCount: s.mailingContactsState === 'ready' ? (Number(s.mailingContactTotal) || 0).toLocaleStr
32320ing('es-AR') : '—',
32321          mailingConsentConfirmed: !!s.mailingConsentConfirmed,
32322          mailingConsentBg: s.mailingConsentConfirmed ?'#edf9f5' : '#fff',
32323          mailingConsentBorder: s.mailingConsentConfirmed ?'#50a98a' : '#cbd8e5',
32324          mailingConsentCheckBg: s.mailingConsentConfirmed ?'#198c68' : '#fff',
32325          mailingConsentChecked: !!s.mailingConsentConfirmed,
32326          mailingImportDisabled: !!s.mailingBusy || !s.mailingConsentConfirmed,
32327          mailingImportOpacity: s.mailingBusy || !s.mailingConsentConfirmed ?'.45' : '1',
32328          mailingCampaignsEmpty: s.mailingCampaignsState === 'ready' && !mailingCampaignList.length,
32329          mailingCampaignsPending: ['idle', 'loading'].includes(s.mailingCampaignsState || 'idle'),
32330          mailingContactsLabel: s.mailingContactsState === 'ready' ? 'contactos cargados' : s.mailingContactsState === 'error' ? 'No se pudo consultar el total' : 'Consultando contactos…',
32331          retryMailing: () => this.fetchMailing(true),
32332          mailingCampaignList,
32333          mailingLaunchInterestCount: (Number(s.mailingLaunchInterestTotal) || 0).toLocaleString('es-AR'),
32334          mailingLaunchAudienceCount: s.mailingLaunchState !== 'ready' ? (s.mailingLaunchState === 'error' ? 'No se pudo consultar' : 'Consultando interesados…') : (Number(s.mailingLaunchInterestTotal) || 0).toLocaleString('es-AR') + ' interesado' + ((Number(s.mailingLaunchInterestTotal) || 0) === 1 ?'' : 's') + ' actualmente',
32335          mailingComposerTitle: mailingSelected ?(mailingSelected.name || mailingSelected.subject || 'Campaña') : 'Nueva campaña',
32336          mailingDraftStatus: mailingSelected ?(mailingStatusLabels[mailingSelected.status] || mailingSelected.status) : 'Sin guardar',
32337          mailingDraftName: mailingDraft.name || '',
32338          mailingDraftSubject: mailingDraft.subject || '',
32339          mailingDraftPreview: mailingDraft.previewText || '',
32340          mailingDraftHtml: mailingDraft.html || '',
32341          mailingDraftText: mailingDraft.text || '',
32342          mailingAudienceLabel: mailingDraft.campaignType === 'launch'
32343            ?'Interesados en el lanzamiento (' + (Number(s.mailingLaunchInterestTotal) || 0).toLocaleString('es-AR') + ')'
32344            : 'Todos los contactos aptos',
32345          mailingAudienceBg: mailingDraft.campaignType === 'launch' ?'#eef9f7' : '#f6f9fb',
32346          mailingAudienceBorder: mailingDraft.campaignType === 'launch' ?'#a9ddd3' : '#d4e0e9',
32347          mailingAudienceColor: mailingDraft.campaignType === 'launch' ?'#126b58' : '#405a72',
32348          mailingAudienceHelp: mailingDraft.campaignType === 'launch'
32349            ?'Esta audiencia es dinámica: al enviar tomará únicamente quienes se hayan anotado y sigan suscriptos.'
32350            : 'Esta campaña utiliza la base general suscripta. La audiencia de lanzamiento es exclusiva de su campaña.',
32351          mailingDraftLocked: mailingLocked,
32352          mailingTestEmail: s.mailingTestEmail || s.user?.email || '',
32353          mailingTestDisabled: !!s.mailingBusy || !mailingSelected?.id,
32354          mailingTestOpacity: s.mailingBusy || !mailingSelected?.id ?'.5' : '1',
32355          mailingSaveDisabled: !!s.mailingBusy || mailingLocked,
32356          mailingSaveOpacity: s.mailingBusy || mailingLocked ?'.5' : '1',
32357          mailingQueueDisabled: !!s.mailingBusy || !mailingSelected?.id || !['draft', 'failed', 'cancelled'].includes(mailingSelected?.status) || !mailingProductionReady,
32358          mailingQueueOpacity: s.mailingBusy || !mailingSelected?.id || !['draft', 'failed', 'cancelled'].includes(mailingSelected?.status) || !mailingProductionReady ?'.5' : '1',
32359          mailingQueueLabel: ['failed', 'cancelled'].includes(mailingSelected?.status)
32360            ?'Reanudar pendientes'
32361            : mailingSelected?.audience === 'launch_interest' ?'Enviar a interesados' : 'Enviar a todos',
32362          mailingEditorShellClass: s.mailingEditorExpanded ?'mailing-editor-fullscreen' : '',
32363          mailingEditorExpanded: !!s.mailingEditorExpanded,
32364          mailingEditorSourceMode: !!s.mailingEditorSourceMode,
32365          mailingEditorVisualMode: !s.mailingEditorSourceMode,
32366          mailingEditorEditable: mailingLocked ?'false' : 'true',
32367          mailingEditorModeLabel: s.mailingEditorSourceMode ?'Editor visual' : 'Ver HTML',
32368          mailingEditorExpandLabel: s.mailingEditorExpanded ?'Reducir' : 'Ampliar',
32369          mailingInsertDialogOpen: !!mailingInsertType,
32370          mailingInsertDialogTitle: mailingInsertIsImage ?'Insertar imagen' : mailingInsertType === 'button' ?'Insertar botón' : 'Insertar enlace',
32371          mailingInsertIsImage,
32372          mailingInsertUrl: s.mailingInsertUrl || '',
32373          mailingInsertText: s.mailingInsertText || '',
32374          mailingInsertUrlLabel: mailingInsertIsImage ?'Imagen subida o URL pública' : 'Destino',
32375          mailingInsertUrlPlaceholder: mailingInsertIsImage ?'https://...' : 'https://sitio.com',
32376          mailingInsertTextLabel: mailingInsertIsImage ?'Texto alternativo' : 'Texto visible',
32377          mailingInsertTextPlaceholder: mailingInsertIsImage ?'Descripción de la imagen' : mailingInsertType === 'button' ?'Ver más' : 'Texto del enlace',
32378          mailingInsertImageSize: mailingImageSize,
32379          mailingInsertImageLink: s.mailingInsertImageLink || '',
32380          mailingImagePreviewMaxWidth: mailingImageSizeWidths[mailingImageSize],
32381          mailingImageLinkBorder: mailingImageLinkValid ?'#cfdbe8' : '#d74a5c',
32382          mailingImageLinkHintColor: mailingImageLinkValid ?'#7a8b9d' : '#b42335',
32383          mailingImageLinkHint: !mailingImageLinkRaw ?'Si lo dejás vacío, la imagen no tendrá enlace.' : mailingImageLinkValid ?'La imagen completa abrirá este destino.' : 'Ingresá una dirección web válida.',
32384          mailingImageUploading: !!s.mailingImageUploading,
32385          mailingImageUploadLabel: s.mailingImageUploading ?'Subiendo y optimizando imagen...' : 'Elegir imagen · JPG, PNG, WEBP o GIF · máximo 8 MB',
32386          mailingImageHasPreview: mailingInsertIsImage && /^https?:\/\//i.test(s.mailingInsertUrl || ''),
32387          mailingImageHasError: !!s.mailingImageError,
32388          mailingImageError: s.mailingImageError || '',
32389          mailingInsertDisabled: !!s.mailingImageUploading || !mailingInsertUrlValid || (mailingInsertIsImage && !mailingImageLinkValid),
32390          mailingInsertOpacity: s.mailingImageUploading || !mailingInsertUrlValid || (mailingInsertIsImage && !mailingImageLinkValid) ?'.5' : '1',
32391          mailingInsertConfirmLabel: mailingInsertIsImage ?'Insertar imagen' : mailingInsertType === 'button' ?'Insertar botón' : 'Insertar enlace',
32392          mailingReportVisible: !!mailingSelected?.id,
32393          mailingReportLoading: !!s.mailingReportLoading,
32394          mailingReportHasError: !!s.mailingReportError,
32395          mailingReportError: s.mailingReportError || '',
32396          mailingReportReady: !!mailingReport && !s.mailingReportLoading,
32397          mailingReportKpis,
32398          mailingReportRates,
32399          mailingReportRows,
32400          mailingReportRowsEmpty: mailingReportRows.length === 0,
32401          mailingReportSearch: s.mailingReportSearch || '',
32402          mailingReportStatusFilter,
32403          mailingReportLetters,
32404          mailingReportFilteredTotal: mailingReportNumber(mailingReportFilteredRows.length),
32405          mailingReportRange: mailingReportFilteredRows.length ?mailingReportNumber(mailingReportPageStart + 1) + '–' + mailingReportNumber(mailingReportPageStart + mailingReportRows.length) : '0',
32406          mailingReportPageLabel: 'Página ' + mailingReportPage + ' de ' + mailingReportPageCount,
32407          mailingReportPrevDisabled: mailingReportPage <= 1,
32408          mailingReportNextDisabled: mailingReportPage >= mailingReportPageCount,
32409          mailingReportPrevOpacity: mailingReportPage <= 1 ?'.45' : '1',
32410          mailingReportNextOpacity: mailingReportPage >= mailingReportPageCount ?'.45' : '1',
32411          onMailingReportSearch: (event) => this.setState({ mailingReportSearch: event.target.value || '', mailingReportPage: 1 }),
32412          onMailingReportStatus: (event) => this.setState({ mailingReportStatusFilter: event.target.value || 'all', mailingReportPage: 1 }),
32413          mailingReportPrev: () => this.setState({ mailingReportPage: Math.max(1, mailingReportPage - 1) }),
32414          mailingReportNext: () => this.setState({ mailingReportPage: Math.min(mailingReportPageCount, mailingReportPage + 1) }),
32415          mailingReportTotal: mailingReportNumber(mailingReportSummary.accepted || mailingReportSummary.sent),
32416          mailingReportExportDisabled: !!s.mailingReportExporting || !mailingSelected?.id,
32417          mailingReportExportOpacity: s.mailingReportExporting || !mailingSelected?.id ?'.55' : '1',
32418          mailingReportExportLabel: s.mailingReportExporting ?'Exportando...' : 'Exportar CSV',
32419          toggleMailing: () => this.toggleMailing(),
32420          newMailingCampaign: () => this.newMailingCampaign(),
32421          newLaunchMailingCampaign: () => this.newLaunchMailingCampaign(),
32422          onMailingName: (event) => this.setMailingDraftField('name', event.target.value),
32423          onMailingSubject: (event) => this.setMailingDraftField('subject', event.target.value),
32424          onMailingPreview: (event) => this.setMailingDraftField('previewText', event.target.value),
32425          onMailingHtml: (event) => this.setMailingDraftField('html', event.target.value),
32426          onMailingHtmlSource: (event) => this.setMailingDraftField('html', this.mailingSanitizeHtml(event.target.value)),
32427          onMailingVisualInput: (event) => this.onMailingVisualInput(event),
32428          onMailingText: (event) => this.setMailingDraftField('text', event.target.value),
32429          onMailingTestEmail: (event) => this.setState({ mailingTestEmail: event.target.value || '' }),
32430          importMailingCsv: (event) => { const file = event.target.files?.[0]; event.target.value = ''; this.importMailingCsvFile(file); },
32431          toggleMailingConsent: () => this.toggleMailingConsent(),
32432          saveMailingDraft: () => this.saveMailingDraft(),
32433          sendMailingTest: () => this.sendMailingTest(),
32434          deleteMailingCampaign: () => this.deleteMailingCampaign(mailingSelected),
32435          queueMailingCampaign: () => this.queueMailingCampaign(),
32436          saveMailingSelection: () => this.saveMailingEditorSelection(),
32437          mailingEditorUndo: (event) => this.execMailingEditorCommand(event, 'undo'),
32438          mailingEditorRedo: (event) => this.execMailingEditorCommand(event, 'redo'),
32439          mailingEditorFormat: (event) => this.execMailingEditorCommand(event, 'formatBlock', event.target.value),
32440          mailingEditorFont: (event) => this.execMailingEditorCommand(event, 'fontName', event.target.value),
32441          mailingEditorFontSize: (event) => this.execMailingEditorCommand(event, 'fontSize', event.target.value),
32442          mailingEditorBold: (event) => this.execMailingEditorCommand(event, 'bold'),
32443          mailingEditorItalic: (event) => this.execMailingEditorCommand(event, 'italic'),
32444          mailingEditorUnderline: (event) => this.execMailingEditorCommand(event, 'underline'),
32445          mailingEditorAlignLeft: (event) => this.execMailingEditorCommand(event, 'justifyLeft'),
32446          mailingEditorAlignCenter: (event) => this.execMailingEditorCommand(event, 'justifyCenter'),
32447          mailingEditorAlignRight: (event) => this.execMailingEditorCommand(event, 'justifyRight'),
32448          mailingEditorBullets: (event) => this.execMailingEditorCommand(event, 'insertUnorderedList'),
32449          mailingEditorNumbers: (event) => this.execMailingEditorCommand(event, 'insertOrderedList'),
32450          mailingEditorColor: (event) => this.execMailingEditorCommand(event, 'foreColor', event.target.value),
32451          mailingEditorBackground: (event) => this.execMailingEditorCommand(event, 'hiliteColor', event.target.value),
32452          mailingEditorDivider: (event) => { event?.preventDefault?.(); this.insertMailingEditorHtml('<hr style="border:0;border-top:1px solid #dce6ef;margin:24px 0">'); },
32453          mailingEditorName: (event) => { event?.preventDefault?.(); this.insertMailingEditorHtml('<span>%recipient.name%</span>'); },
32454          mailingEditorClear: (event) => this.execMailingEditorCommand(event, 'removeFormat'),
32455          openMailingLink: () => this.openMailingInsertDialog('link'),
32456          openMailingButton: () => this.openMailingInsertDialog('button'),
32457          openMailingImage: () => this.openMailingInsertDialog('image'),
32458          closeMailingInsert: () => this.closeMailingInsertDialog(),
32459          onMailingInsertUrl: (event) => this.setState({ mailingInsertUrl: event.target.value || '', mailingImageError: '' }),
32460          onMailingInsertText: (event) => this.setState({ mailingInsertText: event.target.value || '' }),
32461          onMailingImageSize: (event) => this.setState({ mailingInsertImageSize: event.target.value || 'medium' }),
32462          onMailingImageLink: (event) => this.setState({ mailingInsertImageLink: event.target.value || '' }),
32463          uploadMailingImage: (event) => { const file = event.target.files?.[0]; event.target.value = ''; this.uploadMailingEditorImage(file); },
32464          confirmMailingInsert: () => this.confirmMailingInsert(),
32465          toggleMailingSource: () => this.toggleMailingEditorSource(),
32466          toggleMailingExpanded: () => this.toggleMailingEditorExpanded(),
32467          previewMailingHtml: () => this.previewMailingEditor(),
32468          refreshMailingReport: () => this.fetchMailingCampaignReport(),
32469          exportMailingReport: () => this.exportMailingCampaignReport(),
32470          profileUsersSelected: !s.publicAdvanceMode && s.adminUsersOpen ? 'is-selected' : '',
32471          profileMailingSelected: !s.publicAdvanceMode && s.route === 'mailingAdmin' ? 'is-selected' : '',
32472          profileEditorSelected: !s.publicAdvanceMode && s.profileEditorOpen ? 'is-selected' : '',
32473          adminUsersOpen: !s.publicAdvanceMode && !!s.adminUsersOpen,
32474          adminUsersGridCols: s.isMobile ?'1fr' : 'minmax(330px,1fr) minmax(0,1fr)',
32475          adminUsersDivider: s.isMobile ?'0' : '1px solid #e5edf5',
32476          adminUsersMobileDivider: s.isMobile ?'1px solid #e5edf5' : '0',
32477          adminUsersSearch: s.adminUsersSearch || '',
32478          adminUsersLoading: !!s.adminUsersLoading,
32479          adminUsersError: s.adminUsersError || '',
32480          adminUsersHasError: !!s.adminUsersError,
32481          adminUsersEmpty: !s.adminUsersLoading && !s.adminUsersError && Array.isArray(s.adminUsers) && adminUsersList.length === 0,
32482          adminUsersList,
32483          adminDirectoryFilters: directory.filters,
32484          adminDirectorySummary: directory.start + '–' + directory.end + ' de ' + directory.total + ' usuarios',
32485          adminDirectoryGroup: s.adminUsersGroup || 'recent',
32486          adminDirectoryCompany: s.adminUsersConsignataria || '',
32487          adminDirectoryCompanyLogo: directoryCompanyLogo,
32488          adminDirectoryCompanyHasLogo: !!directoryCompanyLogo,
32489          adminDirectoryCompanyLabel: (() => { const company = directory.companies.find(c => c.id === (s.adminUsersConsignataria || '')); return company ? company.label + ' (' + company.count + ')' : 'Todas las consignatarias'; })(),
32490          adminCompanyMenuOpen: !!s.adminCompanyMenuOpen,
32491          adminCompanyMenuExpanded: String(!!s.adminCompanyMenuOpen),
32492          toggleAdminCompanyMenu: () => this.setState({adminCompanyMenuOpen:!s.adminCompanyMenuOpen}),
32493          onAdminCompanyKey: event => { if (event.key === 'Escape') this.setState({adminCompanyMenuOpen:false}); },
32494          adminDirectoryCompanies: directory.companies.map(company => {
32495            const record = (s.adminConsignatarias || []).find(c => String(c.id) === company.id);
32496            const logo = record ? (this.consLogo(record.name || record.nombre || record.id) || record.logoUrl || record.logo || '') : '';
32497            return {...company,logo,hasLogo:!!logo,selected:String(company.id === (s.adminUsersConsignataria || '')),bg:company.id === (s.adminUsersConsignataria || '')?'#edf5fc':'#fff',select:()=>this.setState({adminUsersConsignataria:company.id,adminUsersGroup:'auction',adminUsersPage:1,adminCompanyMenuOpen:false})};
32498          }),
32499          onAdminDirectoryGroup: event => this.setState({adminUsersGroup:event.target.value,adminUsersConsignataria:'',adminUsersPage:1}),
32500          onAdminDirectoryCompany: event => this.setState({adminUsersConsignataria:event.target.value,adminUsersPage:1}),
32501          adminDirectoryCanSort: s.adminUsersGroup === 'all',
32502          adminDirectorySort: s.adminUsersSort || 'alphabetical',
32503          onAdminDirectorySort: event => this.setState({adminUsersSort:event.target.value,adminUsersPage:1}),
32504          adminDirectoryPaginated: directory.pages > 1,
32505          adminDirectoryFirstPage: directory.page === 1,
32506          adminDirectoryLastPage: directory.page === directory.pages,
32507          adminDirectoryPageLabel: directory.page + ' / ' + directory.pages,
32508          adminDirectoryPrevious: () => this.setState({adminUsersPage:Math.max(1,directory.page-1)}),
32509          adminDirectoryNext: () => this.setState({adminUsersPage:Math.min(directory.pages,directory.page+1)}),
32510          adminRecentMissingDates: s.adminUsersGroup === 'recent' && directory.missingDates > 0,
32511          adminMissingDateText: directory.missingDates + ' cuentas sin fecha de alta disponible no se incluyen en este orden.',
32512          refreshAdminDirectory: () => this.fetchAdminUsers(true),
32513          adminUserSelected: !!s.adminSelectedUser,
32514          adminUserNotSelected: !s.adminSelectedUser,
32515          adminSelectedName: s.adminSelectedUser?.displayName || '',
32516          adminSelectedEmail: s.adminSelectedUser?.email || '',
32517          adminRoleOptions,
32518          adminNeedsConsignataria: adminSelectedRoles.some((role) => ['consignatario', 'certificador'].includes(role)),
32519          adminConsignatariaOptions,
32520          adminRelationLoading: !!s.adminUserRelationLoading,
32521          adminUserHasMessage: !!adminUserMessage,
32522          adminUserMessageText: adminUserMessage?.text || '',
32523          adminUserMessageBg: adminUserMessage?.ok ?'#edf9f5' : '#fff0f1',
32524          adminUserMessageBorder: adminUserMessage?.ok ?'#b9e3d4' : '#f2c7cc',
32525          adminUserMessageColor: adminUserMessage?.ok ?'#17664f' : '#b42335',
32526          adminUserSaving: !!s.adminUserSaving || !!s.adminUserRelationLoading,
32527          adminUserSaveLabel: s.adminUserRetrying ?'Esperando al servidor…' : s.adminUserSaving ?'Guardando...' : s.adminUserRelationLoading ?'Consultando...' : 'Guardar permisos',
32528          adminUserSaveOpacity: s.adminUserSaving || s.adminUserRelationLoading ?'.65' : '1',
32529          toggleAdminUsers: () => this.toggleAdminUsers(),
32530          onAdminUsersSearch: (event) => this.setAdminUsersSearch(event.target.value),
32531          saveAdminUser: () => this.saveAdminUserAccess(),
32532          profileEditorOpen: !s.publicAdvanceMode && !!s.profileEditorOpen,
32533          profileEditorSaving: !!s.profileEditorSaving,
32534          profileEditorError: s.profileEditorError || '',
32535          profileEditorHasError: !!s.profileEditorError,
32536          profileEditorFirstName: profileEditorForm.firstName || '',
32537          profileEditorLastName: profileEditorForm.lastName || '',
32538          profileEditorCuit: this.cuitInputDisplay(profileEditorForm.cuitCuil),
32539          onProfileCuit: e => this.setProfileEditorField('cuitCuil',this.cuitInputRaw(e.target.value)),
32540          profileEditorPhoneNumber: profileEditorForm.phoneNumber || '',
32541          profilePhoneReadOnly: !!s.user?.phoneNumber,
32542          profilePhoneBg: s.user?.phoneNumber ?'#eef7f4' : '#fff',
32543          profilePhoneVerified: !!s.user?.phoneNumber,
32544          profilePhoneStatusText: s.user?.phoneNumber ?'Verificado por SMS' : 'Teléfono de contacto.',
32545          profileEditorAvatar,
32546          profileEditorHasAvatar: !!profileEditorAvatar,
32547          profileEditorShowsInitials: !profileEditorAvatar,
32548          profileEditorInitials: (currentProfileName || s.user?.displayName || s.user?.email || 'U').split(/[\s@.]+/).slice(0, 2).map((word) => word[0]?.toUpperCase() || '').join(''),
32549          profileEditorSaveLabel: s.profileEditorSaving ?'Guardando...' : 'Guardar cambios',
32550          profileEditorSaveOpacity: s.profileEditorSaving ?'.65' : '1',
32551          profileCropOpen: !s.publicAdvanceMode && !!s.profileCropOpen,
32552          profileCropZoom: s.profileCropZoom || 1,
32553          profileCropX: s.profileCropX || 50,
32554          profileCropY: s.profileCropY || 50,
32555          openProfileEditor: () => this.openProfileEditor(),
32556          closeProfileEditor: () => this.closeProfileEditor(),
32557          saveProfileEditor: () => this.saveProfileEditor(),
32558          onProfileFirstName: (event) => this.setProfileEditorField('firstName', event.target.value),
32559          onProfileLastName: (event) => this.setProfileEditorField('lastName', event.target.value),
32560          onProfilePhoneNumber: (event) => this.setProfileEditorField('phoneNumber', event.target.value),
32561          onProfilePhoto: (event) => this.selectProfilePhoto(event.target.files?.[0]),
32562          onProfileCropZoom: (event) => this.setProfileCrop('profileCropZoom', event.target.value),
32563          onProfileCropX: (event) => this.setProfileCrop('profileCropX', event.target.value),
32564          onProfileCropY: (event) => this.setProfileCrop('profileCropY', event.target.value),
32565          cancelProfileCrop: () => this.cancelProfileCrop(),
32566          confirmProfileCrop: () => this.confirmProfileCrop(),
32567          notificationsLabel: s.notificationsStatus === 'enabled'  ? 'Notificaciones activas'
32568            : s.notificationsStatus === 'loading'  ? 'Activando...'
32569            : s.notificationsStatus === 'denied'  ? 'Notificaciones bloqueadas'
32570            : s.notificationsStatus === 'unsupported'  ? 'No compatible'
32571            : 'Activar notificaciones',
32572          notificationsTitle: s.notificationsMessage || (s.notificationsStatus === 'denied'
32573             ? 'Habilitalas desde la configuracion del navegador.'
32574            : 'Recibi avisos cuando superen tu oferta.'),
32575          notificationsBg: s.notificationsStatus === 'enabled'  ? 'rgba(31,122,99,0.82)'
32576            : s.notificationsStatus === 'denied' || s.notificationsStatus === 'error'  ? 'rgba(230,57,70,0.75)'
32577            : 'rgba(255,255,255,0.14)',
32578          notificationsBusy: ['enabled', 'loading', 'denied', 'unsupported'].includes(s.notificationsStatus),
32579          notificationsCursor: ['enabled', 'loading', 'denied', 'unsupported'].includes(s.notificationsStatus) ?'default' : 'pointer',
32580          notificationsOpacity: s.notificationsStatus === 'loading' ?0.65 : 1,
32581          enableNotifications: () => this.enableNotifications(),
32582          whatsappNotificationsLabel: s.whatsappNotificationsSaving ?'Guardando...'
32583            : s.whatsappNotificationsLoading ?'Consultando...'
32584            : s.whatsappNotificationsEnabled ?'WhatsApp activo'
32585            : 'Activar WhatsApp',
32586          whatsappNotificationsTitle: s.whatsappNotificationsMessage || (!s.whatsappNotificationsAvailable
32587            ? 'Verifica tu telefono para recibir alertas por WhatsApp.'
32588            : s.whatsappNotificationsEnabled ?'Desactivar alertas por WhatsApp.' : 'Recibi alertas transaccionales por WhatsApp.'),
32589          whatsappNotificationsBg: s.whatsappNotificationsEnabled ?'rgba(31,122,99,0.9)' : 'rgba(255,255,255,0.14)',
32590          whatsappNotificationsBusy: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading,
32591          whatsappNotificationsCursor: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading ?'default' : 'pointer',
32592          whatsappNotificationsOpacity: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading ?0.65 : 1,
32593          toggleWhatsAppNotifications: () => this.toggleWhatsAppNotifications(),
32594          dashCols: s.isMobile ?'1fr' : 'minmax(0,1.7fr) minmax(240px,1fr)',
32595          kpis,
32596          showBuyerCreditPanel: false,
32597          creditGridCols: s.isMobile ?'1fr' : 'minmax(170px,0.8fr) repeat(2,minmax(190px,1fr)) auto',
32598          buyerCreditLoading: s.buyerAccessLoading && buyerCreditRows.length === 0,
32599          buyerCreditHas: buyerCreditRows.length > 0,
32600          buyerCreditEmpty: !s.buyerAccessLoading && buyerCreditRows.length === 0,
32601          buyerCreditRows,
32602          buyerCreditHistory: () => this.openCreditHistory(null, 'buyer'),
32603          sellerCreditHistory: () => this.openCreditHistory(null, 'seller'),
32604          creditHistoryVisible: !!s.creditHistoryOpen && creditHistoryPer
32604spective === 'seller' && canManageBuyers,
32605          creditHistoryTitle: creditHistoryParty,
32606          creditHistoryPartyLabel: creditHistoryPerspective === 'seller' ?'COMPRADOR' : 'VENDEDOR',
32607          creditHistoryRows,
32608          creditHistoryHas: creditHistoryRows.length > 0,
32609          creditHistoryEmpty: creditHistoryRows.length === 0,
32610          closeCreditHistory: () => this.closeCreditHistory(),
32611          closeCreditHistoryBackdrop: (event) => { if (event.target === event.currentTarget) this.closeCreditHistory(); },
32612          stopCreditHistoryClick: (event) => event.stopPropagation(),
32613          creditSummaryVisible: !!s.creditSummaryOpen && creditSummaryPerspective === 'seller' && canManageBuyers,
32614          creditSummaryLoading: !!s.creditSummaryLoading,
32615          creditSummaryHasError: !!s.creditSummaryError && !s.creditSummaryLoading,
32616          creditSummaryError: s.creditSummaryError || '',
32617          creditSummaryReady: !!creditSummary && !s.creditSummaryLoading && !s.creditSummaryError,
32618          creditSummaryTitle: (creditSummaryPerspective === 'seller' ?'Crédito de ' : 'Crédito otorgado por ') + creditSummaryParty,
32619          creditSummarySubtitle: 'Vigencia: ' + creditSummaryPeriod,
32620          creditSummaryStatsCols: s.isMobile ?'1fr' : 'repeat(3,minmax(0,1fr))',
32621          creditSummaryStats: [
32622            { label: 'CRÉDITO AUTORIZADO', value: this.formatBaCurrency(creditSummaryWeekly), bg: '#eef6fd', border: '#cfe1f1', color: '#075ca8' },
32623            { label: 'CRÉDITO USADO', value: this.formatBaCurrency(creditSummaryUsed), bg: '#fff7ea', border: '#efd9ac', color: '#9b6513' },
32624            { label: 'CRÉDITO DISPONIBLE', value: this.formatBaCurrency(creditSummaryAvailable), bg: '#edf9f4', border: '#c9e8db', color: '#1f7a63' },
32625          ],
32626          creditSummaryProgress: creditSummaryPercent.toFixed(2) + '%',
32627          creditSummaryProgressLabel: Math.round(creditSummaryPercentRaw).toLocaleString('es-AR') + '%',
32628          creditSummaryHasMovements: creditMovementRows.length > 0,
32629          creditSummaryEmpty: creditMovementRows.length === 0,
32630          creditSummaryMovements: creditMovementRows,
32631          creditMovementCols: s.isMobile ?'1fr 1fr' : 'minmax(180px,1.35fr) minmax(105px,.65fr) minmax(130px,.8fr) minmax(130px,.8fr)',
32632          closeCreditSummary: () => this.closeCreditSummary(),
32633          closeCreditSummaryBackdrop: (event) => { if (event.target === event.currentTarget) this.closeCreditSummary(); },
32634          stopCreditSummaryClick: (event) => event.stopPropagation(),
32635          refreshCreditSummary: () => this.refreshCreditSummary(),
32636          gridLines: [40, 90, 140, 190],
32637          activityEyebrow: buyerProfile ?'ACTIVIDAD DE OFERTAS' : 'DESEMPEÑO COMERCIAL',
32638          activityTitle: buyerProfile ?'Acumulado por lote' : 'Ofertas recibidas por lote',
32639          activityUnit: buyerProfile ?'ofertas registradas' : 'ofertas recibidas',
32640          activityPrimaryLegend: buyerProfile ?'Mis ofertas' : 'Mis lotes',
32641          activityMarketLegend: 'Promedio del mercado',
32642          activityTotal: fmtK(buyerProfile ?offersMade : receivedOffers),
32643          areaD: this.areaPath(norm(mySeries), 560, 200, 8),
32644          lineD: this.linePath(norm(mySeries), 560, 200, 8),
32645          lineD2: this.linePath(norm(mktSeries), 560, 200, 8),
32646          distributionEyebrow: buyerProfile ?'MIS INTERESES' : 'MI HACIENDA',
32647          distributionTitle: buyerProfile ?'Categorías ofertadas' : 'Por categoría',
32648          distributionUnit: buyerProfile ?'ofertas' : 'cabezas',
32649          donutGradient: distributionTotal ?'conic-gradient(' + stops.join(', ') + ')' : 'conic-gradient(#e8eff6 0deg 360deg)',
32650          donutTotal: fmtK(buyerProfile ?offersMade : totalCabezas),
32651          donutLegend: distributionItems.map((item, i) => ({ label: item.label, color: dColors[i % dColors.length], pct: Math.round(item.count / dTot * 100) + '%' })),
32652          showPriceStats: priceCats.length > 0,
32653          priceBars: priceCats.map(([label, mineV, mkt], i) => ({
32654            label, mine: '$' + fmtK(mineV), market: '$' + fmtK(mkt),
32655            minePct: (mineV / pMax * 100).toFixed(0) + '%', marketPct: (mkt / pMax * 100).toFixed(0) + '%',
32656            delay: (i * 0.08 + 0.2).toFixed(2) + 's', delay2: (i * 0.08 + 0.35).toFixed(2) + 's',
32657          })),
32658          showProfileTabs: !buyerProfile,
32659          profileTabs: (() => {
32660            const defs = [];
32661            if (this.isRosganProfile()) defs.push(['resumen', 'Próximos lotes', (s.profileRosganLots || []).length]);
32662            if (canSell && !this.isRosganProfile()) defs.push(['resumen', 'Mis lotes', mine.length]);
32663            if (canSell || this.isCertifierOnly(s.apiUser)) defs.push(['comitentes', 'Comitentes', comitenteDirectory.total], ['establecimientos', 'Establecimientos', estData.length]);
32664            if (canBuy && !this.isRosganProfile()) defs.push(['ofertas', 'Mis ofertas', offerData.length]);
32665            if (canSell && !this.isRosganProfile() && (pendData.length || s.pendingBidsLoading)) defs.push(['aprobar', 'Por aprobar', pendData.length]);
32666            return defs.map(([key, label, count]) => ({
32667              label, count, go: () => this.setState({ profileTab: key }),
32668              bg: s.profileTab === key ?'#002e5f' : '#fff', color: s.profileTab === key ?'#fff' : '#3d4c5f', border: s.profileTab === key ?'#002e5f' : '#e3eaf2',
32669            }));
32670          })(),
32671          profileShowLots: !this.isRosganProfile() && s.profileTab === 'resumen' && canSell, profileShowOffers: !buyerProfile && s.profileTab === 'ofertas' && canBuy, profileShowPending: s.profileTab === 'aprobar' && canSell, profileShowComitentes: s.profileTab === 'comitentes' && (canSell || this.isCertifierOnly(s.apiUser)), profileShowEst: s.profileTab === 'establecimientos' && (canSell || this.isCertifierOnly(s.apiUser)),
32672          profileCanEditRelations: this.canManageComitentes(s.apiUser),
32673          profileIsDev: this.userRoleIds(s.apiUser).includes('dev'),
32674          profileCurrentVersionLabel: s.devCurrentVersion ? (String(s.devCurrentVersion).startsWith('local-') ? 'Versión vigente · Entorno local · ' : 'Versión vigente · ') + String(s.devCurrentVersion).replace(/^rosganapp-build-/, '') : 'Consultando versión vigente…',
32675          openDevActivity: () => { if (this.userRoleIds(this.state.apiUser).includes('dev')) { window.RosganDevActivity.start(this); this.go('devActivity'); } },
32676          openProfileComitenteMerge: () => this.openComitenteMerge(),
32677          profileShowBuyers: !this.isRosganProfile() && canManageBuyers,
32678          launchAdminVisible: canManageAdmin,
32679          launchAdminLoading: !!s.launchInterestsAdminLoading && s.launchInterestsAdmin === null,
32680          launchAdminHasError: !!s.launchInterestsAdminError,
32681          launchAdminError: s.launchInterestsAdminError || '',
32682          launchAdminHasRows: launchAdminRows.length > 0,
32683          launchAdminEmpty: !s.launchInterestsAdminLoading && !s.launchInterestsAdminError && launchAdminRows.length === 0,
32684          launchAdminRows,
32685          launchAdminCountLabel: (Number(s.launchInterestsAdminTotal) || launchAdminRows.length).toLocaleString('es-AR') + ' persona' + ((Number(s.launchInterestsAdminTotal) || launchAdminRows.length) === 1 ?'' : 's') + ' anotada' + ((Number(s.launchInterestsAdminTotal) || launchAdminRows.length) === 1 ?'' : 's'),
32686          launchAdminExportDisabled: !launchAdminRows.length,
32687          launchAdminExportOpacity: launchAdminRows.length ?'1' : '.45',
32688          exportLaunchInterests: () => this.exportLaunchInterests(),
32689          lotYieldAdminVisible: this.isSysadmin(s.apiUser),
32690          lotYieldLoading: !!s.lotYieldLoading && s.lotYieldCategories === null,
32691          lotYieldHasError: !!s.lotYieldError,
32692          lotYieldError: s.lotYieldError || '',
32693          lotYieldHasRows: lotYieldRows.length > 0,
32694          lotYieldEmpty: !s.lotYieldLoading && !s.lotYieldError && lotYieldRows.length === 0,
32695          lotYieldRows,
32696          lotYieldHasMessage: !!s.lotYieldMessage,
32697          lotYieldMessageText: s.lotYieldMessage?.text || '',
32698          lotYieldMessageBg: s.lotYieldMessage?.ok ?'#edf9f5' : '#fff0f1',
32699          lotYieldMessageBorder: s.lotYieldMessage?.ok ?'#b9e3d4' : '#f2c7cc',
32700          lotYieldMessageColor: s.lotYieldMessage?.ok ?'#17664f' : '#b42335',
32701          refreshLotYields: () => this.loadLotYieldCategories(true),
32702          buyerDirectorySearch: s.buyerDirectorySearch || '',
32703          setBuyerDirectorySearch: (event) => this.setState({ buyerDirectorySearch: event.target.value }),
32704          buyerDirectoryLoading: s.buyerAccountsLoading && !buyerDirectoryData.length,
32705          buyerDirectoryHas: buyerDirectoryData.length > 0,
32706          buyerDirectoryEmpty: !s.buyerAccountsLoading && buyerDirectoryData.length === 0,
32707          buyerDirectoryEmptyText: buyerDirectoryQuery ?'No hay compradores que coincidan con la búsqueda.' : 'No hay cuentas con rol comprador.',
32708          buyerDirectoryRows: buyerDirectoryData,
32709          baLoading: s.buyerAuthsLoading && s.buyerAuths === null,
32710          baHas: baData.length > 0, baEmpty: !s.buyerAuthsLoading && baData.length === 0,
32711          baFormOpen: s.baFormOpen,
32712          toggleBaForm: () => this.setState({ baFormOpen: !s.baFormOpen, baForm: s.baFormOpen ?s.baForm : this.emptyBaForm(), baFormMessage: null }),
32713          ba_selectedBuyer: baForm.buyerEmail || 'Seleccioná un comprador del directorio',
32714          ba_creditStartDate: baForm.creditStartDate, set_ba_creditStartDate: (e) => this.setBaForm('creditStartDate', e.target.value),
32715          ba_weeklyCreditAmount: this.moneyInputDisplay(baForm.weeklyCreditAmount), set_ba_weeklyCreditAmount: (e) => this.formatMoneyInputEvent(e, (raw) => this.setBaForm('weeklyCreditAmount', raw)),
32716          baSaving: s.baSaving, baSaveLabel: s.baSaving ?'Creando crédito…' : 'Crear crédito',
32717          baFormMsg: s.baFormMessage?.text || '', baFormMsgBg: s.baFormMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', baFormMsgColor: s.baFormMessage?.ok ?'#1f7a63' : '#c22',
32718          saveBuyerAuth: () => this.saveBuyerAuth(),
32719          baRows: baData,
32720          profileLotsLoading: s.myLotsLoading && mine.length === 0,
32721          hasMyLots: mine.length > 0, myLotsEmpty: !s.myLotsLoading && mine.length === 0,
32722          myLotCards: mineSorted.map((l) => {
32723            const bid = bidFor(l);
32724            const prog = Math.min(100, Math.round(bid / (l.baseNum || bid || 1
32724) * 88 + 12));
32725            const expired = l.status === 'Caducado';
32726            const canRepublish = expired && l.canRepublish;
32727            const offerCount = Math.max(Number(l.bidCount) || 0, (l.bids || []).length, s.bidsPlaced[l.cert] ?1 : 0);
32728            const isSold = l.auctionStatus === 'sold' || l.status === 'Vendido';
32729            const isClosedWithoutBids = l.auctionStatus === 'closed_no_bids';
32730            const isOpen = l.auctionStatus === 'open' || l.status === 'En vivo';
32731            const canEdit = !expired && !isSold && !isClosedWithoutBids;
32732            const bidTitle = isSold ?'PRECIO FINAL' : (isOpen && offerCount > 0 ?'OFERTA LÍDER' : 'PRECIO BASE');
32733            const readyForLoading = isSold && !!l.fechaDisponibleCarga && l.fechaDisponibleCarga <= this.localIsoDate();
32734            const progressLabel = isSold
32735              ?(offerCount === 1 ?'Venta cerrada · 1 oferta' : 'Venta cerrada · ' + offerCount + ' ofertas')
32736              : isClosedWithoutBids
32737                 ? 'Cerrado sin ofertas'
32738                : isOpen && offerCount > 0
32739                  ?(offerCount === 1 ?'1 oferta recibida' : offerCount + ' ofertas recibidas')
32740                  : l.auctionStatus === 'scheduled'
32741                     ? 'Subasta programada'
32742                    : 'Esperando ofertas';
32743            return { open: () => this.openLot(l.cert), image: l.image, categoria: l.categoria, cert: l.cert, origen: l.origen,
32744              bid: this.fmtARS(bid), bidTitle, cabezas: String(l.cabezas), progress: prog + '%', progressLabel,
32745              expired, notExpired: !expired, canRepublish, canEdit,
32746              edit: (event) => { event.stopPropagation(); this.startEditLot(l); },
32747              republish: (event) => { event.stopPropagation(); this.openRepublish(l); },
32748              ...stateMap(readyForLoading ?'Listo para la carga' : l.status) };
32749          }),
32750          republishOpen: !!s.republishLot,
32751          republishTitle: s.republishLot ?(s.republishLot.categoria + ' · Cert. ' + s.republishLot.cert) : '',
32752          closeRepublish: () => this.closeRepublish(),
32753          rp_fecha: republishForm.fechaSubasta || '', set_rp_fecha: (e) => this.setRepublish('fechaSubasta', e.target.value),
32754          rp_listoParaCargar: republishForm.listoParaCargar === true,
32755          rp_listoLabel: republishForm.listoParaCargar === true ?'✓ Listo para cargar desde la subasta' : '○ Listo para cargar desde la subasta',
32756          rp_listoBg: republishForm.listoParaCargar === true ?'#e6f5ef' : '#f5f8fc', rp_listoBorder: republishForm.listoParaCargar === true ?'#1f7a63' : '#dce6f0', rp_listoColor: republishForm.listoParaCargar === true ?'#17664f' : '#3d4c5f',
32757          toggleRpListoParaCargar: () => this.setRepublish('listoParaCargar', republishForm.listoParaCargar !== true),
32758          rp_showFechaCarga: republishForm.listoParaCargar !== true,
32759          rp_fechaCargaMin: this.loadDateOffset(republishForm.fechaSubasta, 1), rp_fechaCargaMax: this.loadDateOffset(republishForm.fechaSubasta, 7),
32760          rp_fechaCarga: republishForm.fechaDisponibleCarga || '', set_rp_fechaCarga: (e) => this.setRepublish('fechaDisponibleCarga', e.target.value),
32761          rp_precio: this.moneyInputDisplay(republishForm.precioSugerido), set_rp_precio: (e) => this.setRepublish('precioSugerido', this.moneyInputRaw(e.target.value)),
32762          rp_plazo: republishForm.plazoId || '', set_rp_plazo: (e) => this.setRepublish('plazoId', e.target.value),
32763          rp_plazoOtro: republishForm.plazoOtro || '', set_rp_plazoOtro: (e) => this.setRepublish('plazoOtro', e.target.value),
32764          rpPlazoOpts: (this.CATALOGS.termOptions || []).map(([id, label]) => ({ id, label })),
32765          rpShowOtro: republishForm.plazoId === 'otro',
32766          rpSaving: s.republishSaving, rpSaveLabel: s.republishSaving ?'Republicando…' : 'Republicar lote',
32767          submitRepublish: () => this.submitRepublish(),
32768          rpMsg: s.republishMessage?.text || '', rpMsgBg: s.republishMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', rpMsgColor: s.republishMessage?.ok ?'#1f7a63' : '#c22',
32769          myBidsLoading: s.myBidsLoading && !offerData.length,
32770          hasOffers: offerData.length > 0, offersEmpty: !s.myBidsLoading && offerData.length === 0,
32771          offerRows: offerData,
32772          showLiveOffersPanel: buyerProfile,
32773          livePanelStatus: liveOffers.length ?'EN VIVO' : 'SIN OFERTAS ABIERTAS',
32774          livePanelStatusColor: liveOffers.length ?'#1f7a63' : '#65758a',
32775          livePanelStatusBg: liveOffers.length ?'rgba(31,122,99,0.1)' : 'rgba(101,117,138,0.1)',
32776          livePanelDotAnimation: liveOffers.length ?'pulseDot 1.2s infinite' : 'none',
32777          hasLiveOffers: liveOffers.length > 0,
32778          liveOffersLoading: canBuy && s.myBidsLoading && liveOffers.length === 0,
32779          liveOffersEmpty: !s.myBidsLoading && liveOffers.length === 0 && buyerClosedAllRows.length === 0,
32780          liveOffers,
32781          completedOffers,
32782          buyerClosedHas: buyerClosedAllRows.length > 0,
32783          buyerClosedCount: buyerClosedAllRows.length,
32784          buyerClosedOfferRows,
32785          buyerClosedPaginated: buyerClosedPages > 1,
32786          buyerClosedPageLabel: 'Página ' + buyerClosedPage + ' de ' + buyerClosedPages,
32787          buyerClosedPrevDisabled: buyerClosedPage <= 1,
32788          buyerClosedNextDisabled: buyerClosedPage >= buyerClosedPages,
32789          buyerClosedPrev: () => this.setState({ buyerClosedPage: Math.max(1, buyerClosedPage - 1) }),
32790          buyerClosedNext: () => this.setState({ buyerClosedPage: Math.min(buyerClosedPages, buyerClosedPage + 1) }),
32791          showSellerOffersPanel: canSell,
32792          sellerOffersStatus: (() => {
32793            const liveCount = sellerOpenOfferRows.length;
32794            const finalCount = sellerClosedAllRows.length;
32795            return [liveCount ?liveCount + ' en vivo' : '', finalCount ?finalCount + ' cerrados' : ''].filter(
32795Boolean).join(' · ') || 'SIN OFERTAS';
32796          })(),
32797          sellerOffersStatusColor: sellerOpenOfferRows.length ?'#1f7a63' : '#65758a',
32798          sellerOffersStatusBg: sellerOpenOfferRows.length ?'rgba(31,122,99,0.1)' : 'rgba(101,117,138,0.1)',
32799          sellerOffersStatusAnimation: sellerOpenOfferRows.length ?'pulseDot 1.2s infinite' : 'none',
32800          sellerOffersLoading: sellerOpenLots.some((lot) => !!s.lotBidsLoading[lot.cert]) && sellerOfferRows.length === 0,
32801          sellerOpenOffersEmpty: !sellerOpenLots.some((lot) => !!s.lotBidsLoading[lot.cert]) && sellerOpenOfferRows.length === 0,
32802          sellerOpenOfferRows,
32803          sellerClosedHas: sellerClosedAllRows.length > 0,
32804          sellerClosedCount: sellerClosedAllRows.length,
32805          sellerClosedOfferRows,
32806          sellerClosedPaginated: sellerClosedPages > 1,
32807          sellerClosedPageLabel: 'Página ' + sellerClosedPage + ' de ' + sellerClosedPages,
32808          sellerClosedPrevDisabled: sellerClosedPage <= 1,
32809          sellerClosedNextDisabled: sellerClosedPage >= sellerClosedPages,
32810          sellerClosedPrev: () => this.setState({ sellerClosedPage: Math.max(1, sellerClosedPage - 1) }),
32811          sellerClosedNext: () => this.setState({ sellerClosedPage: Math.min(sellerClosedPages, sellerClosedPage + 1) }),
32812          pendingLoading: s.pendingBidsLoading && !pendData.length,
32813          hasPending: pendData.length > 0, pendingEmpty: !s.pendingBidsLoading && pendData.length === 0,
32814          pendingRows: pendData,
32815          comitentesLoading: s.comitentesLoading && !comitenteData.length,
32816          comitentesHas: comitenteData.length > 0,
32817          comitentesEmpty: !s.comitentesLoading && !comitenteData.length,
32818          comitenteRows: comitenteData,
32819          pcQuery: s.profileComitenteQuery || '', pcProvince: s.profileComitenteProvince || '', pcLocality: s.profileComitenteLocality || '',
32820          pcProvinces: comitenteDirectory.provinces.map(value => ({value})), pcLocalities: comitenteDirectory.localities.map(value => ({value})),
32821          pcSearch: e => this.setState({profileComitenteQuery:e.target.value,profileComitentePage:1}),
32822          pcSetProvince: e => this.setState({profileComitenteProvince:e.target.value,profileComitenteLocality:'',profileComitentePage:1}),
32823          pcSetLocality: e => this.setState({profileComitenteLocality:e.target.value,profileComitentePage:1}),
32824          pcClear: () => this.setState({profileComitenteQuery:'',profileComitenteProvince:'',profileComitenteLocality:'',profileComitentePage:1}),
32825          pcHasFilters: !!(s.profileComitenteQuery || s.profileComitenteProvince || s.profileComitenteLocality),
32826          pcCount: comitenteDirectory.count + ' de ' + comitenteDirectory.total + ' comitentes',
32827          pcEmptyLabel: comitenteDirectory.total ? 'No hay coincidencias con esta búsqueda' : 'Todavía no tenés comitentes',
32828          pcPaginated: comitenteDirectory.pages > 1,
32829          pcPageLabel: 'Página ' + comitenteDirectory.page + ' de ' + comitenteDirectory.pages,
32830          pcPrevDisabled: comitenteDirectory.page <= 1, pcNextDisabled: comitenteDirectory.page >= comitenteDirectory.pages,
32831          pcPrev: () => this.setState({profileComitentePage:comitenteDirectory.page-1}),
32832          pcNext: () => this.setState({profileComitentePage:comitenteDirectory.page+1}),
32833          comitenteFormOpen: !!s.comitenteFormOpen,
32834          toggleComitenteForm: () => { const opening = !s.comitenteFormOpen; if (opening) this.fetchProvinces(); this.setState({ comitenteFormOpen: opening, comitenteForm: opening ?this.emptyComitenteForm() : s.comitenteForm, comitenteFormMessage: null }); },
32835          cf_cuitCuil: comitenteForm.cuitCuil || '', set_cf_cuitCuil: (event) => this.onComitenteCuit(event.target.value),
32836          cf_alias: comitenteForm.alias || '', set_cf_alias: e => this.setComitenteForm('alias', e.target.value),
32837          cf_nombreFiscal: comitenteForm.nombreFiscal || '', cf_provincia: comitenteForm.provincia || '', cf_localidad: comitenteForm.localidad || '',
32838          cfLookupStatus: s.comitenteLookupLoading ?'Consultando ARCA…' : (comitenteForm.source ?'Datos verificados' : ''),
32839          comitenteSave: () => this.saveComitente(),
32840          comitenteSaveDisabled: !!s.comitenteSaving || !!s.comitenteLookupLoading || !String(comitenteForm.alias || '').trim(),
32841          comitenteSaveLabel: s.comitenteSaving ?'Creando…' : (s.comitenteLookupLoading ?'Consultando…' : 'Crear comitente'),
32842          comitenteFormMsg: s.comitenteFormMessage?.text || '',
32843          comitenteFormMsgBg: s.comitenteFormMessage?.ok ?'#edf9f5' : '#fff0f1',
32844          comitenteFormMsgColor: s.comitenteFormMessage?.ok ?'#17664f' : '#b42335',
32845          estLoading: s.establishmentsLoading && !estData.length,
32846          estHas: estData.length > 0, estEmpty: !s.establishmentsLoading && estData.length === 0,
32847          estRowsList: estData,
32848          estFormOpen: s.estFormOpen,
32849          toggleEstForm: () => { if (!this.canManageComitentes(s.apiUser)) return; const opening = !s.estFormOpen; if (opening) this.fetchProvinces(); this.setState({ estFormOpen: opening, estForm: s.estFormOpen ?s.estForm : this.emptyEstForm(), estFormMessage: null }); },
32850          ef_nombre: ef.nombre, set_ef_nombre: (e) => this.setEstForm('nombre', e.target.value),
32851          ef_comitenteId: ef.comitenteId || '', set_ef_comitenteId: (e) => this.setEstForm('comitenteId', e.target.value),
32852          efComitenteOpts: comitenteData.map((item) => ({ id: item.id, label: item.razonSocial + ' · ' + item.cuitCuil })),
32853          ef_renspa: ef.renspa || '', set_ef_renspa: (e) => this.setEstForm('renspa', e.target.value),
32854          ef_provincia: ef.provincia, set_ef_provincia: (e) => this.setEstForm('provincia', e.target.value),
32855          ef_localidad: ef.localidad, set_ef_localidad: (e) => this.onEstPlaceInput(e.target.value),
32856          ef_latitud: ef.latitud, set_ef_latitud: (e) => this.setEstForm('latitud', e.target.value),
32857          ef_longitud: ef.longitud, set_ef_longitud: (e) => this.setEstForm('longitud', e.target.value),
32858          efProvinceOpts: s.geoProvinces || [], efProvPlaceholder: s.geoLoadingProv ?'Cargando provincias…' : 'Seleccionar…',
32859          efLocalityOpts: [],
32860          efLocDisabled: !ef.provincia, efLocBg: !ef.provincia ?'#f2f6fb' : '#fff',
32861          efLocPlaceholder: !ef.provincia ?'Elegí provincia primero' : 'Escribí una localidad',
32862          efPlaceLoading: !!s.estPlaceLoading,
32863          efPlaceResultsOpen: !!(s.estPlaceResults || []).length,
32864          efPlaceResults: (s.estPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectEstPlace(item) })),
32865          openEfPlaces: () => { if (ef.provincia && String(ef.localidad || '').trim().length >= 3) this.searchEstPlaces(ef.localidad, ef.provincia); },
32866          closeEfPlaces: () => this.closeEstPlaceResults(),
32867          efCoordsNote: (ef.latitud && ef.longitud) ?'Coordenadas autocompletadas desde la localidad' : 'Se completan al elegir la localidad',
32868          saveEst: () => this.saveEstablishment(),
32869          estSaveLabel: s.estSaving ?'Guardando…' : 'Guardar establecimiento',
32870          estSaveDisabled: s.estSaving,
32871          estSaveCursor: s.estSaving ?'not-allowed' : 'pointer',
32872          estSaveOpacity: s.estSaving ?'0.55' : '1',
32873          estFormMsg: s.estFormMessage?.text || '', estFormMsgBg: s.estFormMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', estFormMsgColor: s.estFormMessage?.ok ?'#1f7a63' : '#c22',
32874        };
32875      })(),
32876      // ---- feria creator ----
32877      goFeria: () => this.go('feria'),
32878      goFeriaConfig: () => this.go('feriaConfig'),
32879      ...(() => {
32880        const ff = s.feriaForm;
32881        const phase = s.feriaPhase;
32882        const canChooseConsignataria = this.canManageUsers(s.apiUser);
32883        const assignedConsignataria = this.feriaAssignedConsignataria();
32884        const allConsignatarias = (s.consignatarias || []).map((c) => ({ ...c, name: this.consName(c) })).filter((c) => c.name).sort((a, b) => a.name.localeCompare(b.name, 'es'));
32885        const cons = canChooseConsignataria ?allConsignatarias : (assignedConsignataria ?[{ ...assignedConsignataria, name: this.consName(assignedConsignataria) || assignedConsignataria.name }] : []);
32886        const provs = s.geoProvinces;
32887        const res = s.feriaResult || {};
32888        const resVid = this.ytId(res.youtubeUrl);
32889        const fields = ['consignataria', 'provincia', 'localidad', 'fecha', 'hora', 'cabezas'];
32890        const done = fields.filter((k) => String(ff[k] || '').length).length;
32891        const pvVid = this.ytId(ff.youtube) || (s.videos || [])[0]?.id;
32892        const dfmt = (iso) => { const d = this.feriaDate({fecha:iso}); return !d ?'' : d.toLocaleDateString('es-AR', { day: 'numeric', month: 'short', year: 'numeric' }); };
32893        const fechaLabel = ff.fecha ?new Date(ff.fecha + 'T' + (ff.hora || '00:00')).toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) : '—';
32894        return {
32895          feriaDevicesAllowed: canChooseConsignataria,
32896          controlFeriaYoutube: () => window.RosganFeriaYoutube.list({apiFetch:(path,opts)=>this.apiFetch(path,opts),consLogo:name=>this.consLogo(name)}),
32897          feriaCols: s.isMobile ?'1fr' : 'minmax(0,1.35fr) minmax(300px,1fr)',
32898          feriaGrid2: s.isMobile ?'1fr' : '1fr 1fr',
32899          feriaGrid3: s.isMobile ?'1fr 1fr' : '1fr 1fr 1fr',
32900          feriaThumbnailData: JSON.stringify({ ...ff, nombreRemate: ff.nombre || (ff.localidad ? 'Feria ' + ff.localidad : 'Remate feria') }),
32901          feriaPreviewPos: s.isMobile ?'static' : 'sticky',
32902          feriaProcessing: phase !== 'idle',
32903          feriaBusy: phase === 'connecting' || phase === 'creating',
32904          feriaDone: phase === 'done' && this.feriaYoutubeReady(res), feriaFailed: phase === 'error' || phase === 'waiting', feriaError: s.feriaError,
32905          feriaCanRetry: !!res.id, feriaFailureTitle: res.id ? 'Feria guardada · transmisión pendiente' : 'No pudimos crear la feria',
32906          feriaRetryLabel: phase === 'error' && res.youtubeStatus === 'failed' ? 'Reintentar transmisión' : 'Consultar estado',
32907          retryFeriaYoutube: () => this.retryFeriaYoutube(),
32908          deleteFeria: () => this.deleteSavedFeria(res), feriaDeleting: !!s.feriaDeleting, feriaDeleteLabel: s.feriaDeleting ? 'Eliminando…' : 'Eliminar feria',
32909          feriaSteps: [
32910            { label: 'Conectando', key: 'connecting' }, { label: 'Creando vivo', key: 'creating' }, { label: 'Portada', key: 'done' },
32911          ].map((st, i) => {
32912            const order = { connecting: 0, creating: 1, done: 2 };
32913            const cur = order[phase] || 0; const idx = i;
32914            const active = idx === cur; const complete = idx < cur;
32915            return { label: st.label, color: active || complete ?'#fff' : 'rgba(255,255,255,0.4)',
32916              dotBg: complete ?'#5fd68f' : active ?'#fff' : 'rgba(255,255,255,0.14)', dotColor: complete ?'#fff' : active ?'#002e5f' : '#fff',
32917              done: complete, numberVisible: !complete, number: String(idx + 1) };
32918          }),
32919          feriaPhaseTitle: phase === 'creating' ?'Creando tu remate' : 'Conectando con Rosgan',
32920          feriaPhaseText: phase === 'creating' ?'Preparando el vivo, vinculando la transmisión y generando la portada en YouTube.' : 'Estableciendo conexión con los servidores de transmisión.',
32921          resultThumb: resVid ?'https://i.ytimg.com/vi/' + resVid + '/maxresdefault.jpg' : this.consLogo(res.consignataria) || 'assets/feria.png',
32922          resultLogo: this.consLogo(res.consignataria), resultHasLogo: !!this.consLogo(res.consignataria),
32923          resultTitle: res.nombreRemate || res.title || 'Remate feria', resultUrl: res.youtubeUrl || '',
32924          resultCons: res.consignataria || '—', resultDate: dfmt(res.fecha) || 'A confirmar', resultDemo: false,
32925          copyResult: () => this.copyText(res.youtubeUrl || ''),
32926          backFromFeriaResult: () => { this.resetFeriaCreator(); this.go('profile'); this.refreshProfileData(true); },
32927          resetCreator: () => this.resetFeriaCreator(),
32928          consChips: cons.map((c) => {
32929            const name = c.name || c.nombre || 'Consignataria';
32930            const logo = this.consLogo(name) || c.logoUrl || c.logo || c.imageUrl || '';
32931            return { name, pick: () => this.chooseFeriaConsignataria(name, logo), logo, whiteLogo: this.consWhiteLogo(name) || logo, logoBg: this.consBrandColor(name) || '#002e5f',
32932              cursor: canChooseConsignataria ?'pointer' : 'default', bg: ff.consignataria === name ?'rgba(31,122,99,0.08)' : '#f6f9fc', border: ff.consignataria === name ?'#1f7a63' : '#eef3f9' };
32933          }),
32934          feriaConsScopeText: canChooseConsignataria ?'' : (assignedConsignataria ?'La consignataria está fijada por la asignación de tu usuario.' : 'Tu usuario no tiene una consignataria activa asignada.'),
32935          feriaConsScopeColor: assignedConsignataria || canChooseConsignataria ?'#65758a' : '#c62f3d',
32936          feriaHasConsignataria: !!ff.consignataria,
32937          feriaFavoriteColor: this.consBrandColor(ff.consignataria),
32938          feriaFavoriteLogo: ff.consignatariaLogo || this.consLogo(ff.consignataria) || '',
32939          feriaManualOpen: !!s.feriaManualOpen,
32940          feriaFavoritesHint: this.feriaFavoritesFor(ff.consignataria).length ?'Elegí dónde será tu próximo remate.' : 'Sin fer
32940ias favoritas. Tocá “Usar otra ubicación” para cargar una.',
32941          feriaFavorites: this.feriaFavoritesFor(ff.consignataria).map(f => ({ ...f, selected:s.feriaFavoriteId === f.id, border:s.feriaFavoriteId === f.id ?'#1f7a63':'#e3eaf2', bg:s.feriaFavoriteId === f.id ?'#eef8f3':'#f6f9fc', pick:() => this.chooseFeriaFavorite(f.id) })),
32942          feriaManualLocation: () => this.clearFeriaFavorite(),
32943          provinceOpts: provs || [], provPlaceholder: s.geoLoadingProv ?'Cargando provincias…' : 'Seleccionar…',
32944          locDisabled: !ff.provincia, locBg: !ff.provincia ?'#f2f6fb' : '#fff',
32945          locPlaceholder: !ff.provincia ?'Elegí provincia primero' : 'Escribí una localidad',
32946          ff_provincia: ff.provincia, ff_setProvincia: (e) => this.setFF('provincia', e.target.value),
32947          ff_localidad: ff.localidad, ff_setLocalidad: (e) => this.onFeriaPlaceInput(e.target.value),
32948          ff_openLocalidades: () => { if (ff.provincia && String(ff.localidad || '').trim().length >= 3) this.searchFeriaPlaces(ff.localidad, ff.provincia); },
32949          ff_closeLocalidades: () => this.closeFeriaPlaceResults(),
32950          feriaPlaceLoading: !!s.feriaPlaceLoading,
32951          feriaPlaceResultsOpen: !!(s.feriaPlaceResults || []).length,
32952          feriaPlaceResults: (s.feriaPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectFeriaPlace(item) })),
32953          feriaPlaceError: s.feriaPlaceError || '',
32954          ff_nombre: ff.nombre, ff_setNombre: (e) => this.setFF('nombre', e.target.value),
32955          ff_fecha: ff.fecha, ff_setFecha: (e) => this.setFF('fecha', e.target.value),
32956          ff_hora: ff.hora, ff_setHora: (e) => this.setFF('hora', e.target.value),
32957          ff_cabezas: ff.cabezas, ff_setCabezas: (e) => this.setFF('cabezas', e.target.value),
32958          feriaFormMessage: s.feriaFormMessage?.text || '',
32959          feriaBtnBg: this.feriaValid() ?'#1f7a63' : '#9fb4ab',
32960          cancelFeriaEdit: () => { this.resetFeriaCreator(); this.go('profile'); }, feriaHeading: s.feriaEditingId ? 'Editar remate feria' : 'Crear remate feria', feriaSubmitLabel: s.feriaEditingId ? 'Guardar cambios' : 'Generar transmisión', feriaEditingId: s.feriaEditingId || null, submitFeria: () => this.submitFeria(),
32961          shareFeriaFlyer: () => this.shareFeriaFlyer(),
32962          pvBaseColor: this.consBrandColor(ff.consignataria),
32963          pvGradA: 'color-mix(in srgb, ' + this.consBrandColor(ff.consignataria) + ', white 18%)',
32964          pvGradB: 'color-mix(in srgb, ' + this.consBrandColor(ff.consignataria) + ', black 30%)',
32965          pvFeriaLogo: ff.consignatariaLogo || this.consLogo(ff.consignataria), pvFeriaHasLogo: !!(ff.consignatariaLogo || this.consLogo(ff.consignataria)), pvFeriaNoLogo: !(ff.consignatariaLogo || this.consLogo(ff.consignataria)),
32966          pvFeriaInitial: (ff.consignataria || 'R').trim()[0].toUpperCase(),
32967          pvFeriaTitle: ff.nombre || (ff.localidad ?'Feria ' + ff.localidad : 'Nuevo remate feria'),
32968          pvFeriaMeta: [ff.consignataria, [ff.localidad, ff.provincia].filter(Boolean).join(', ')].filter(Boolean).join(' · ') || 'Completá los datos',
32969          pvFeriaCabezas: ff.cabezas ?Number(ff.cabezas).toLocaleString('es-AR') : '—',
32970          pvFeriaFecha: fechaLabel,
32971          feriaPct: Math.round(done / fields.length * 100) + '%',
32972          hasCreatedFerias: (s.feriaCreated || []).length > 0,
32973          createdFeriaList: (s.feriaCreated || []).slice(0, 4).map((cf) => ({ title: cf.nombreRemate || cf.title || 'Remate feria', href: cf.youtubeUrl || 'https://www.youtube.com/@rosgantv' })),
32974        };
32975      })(),
32976    };
32977  }
32978}
32979</script>
vendor: 1 bytes, line 32979
32979
32980<script id="lacassina-public-stats-v2-flag">window.__ROSGAN_PUBLIC_STATS_V2__ = true;</script>
vendor: 1 bytes, line 32980
32980
32981<script id="lacassina-public-stats-script">
32982(function(){
32983  if (window.__ROSGAN_PUBLIC_ROUTE__ !== 'lacassina-stats' || window.__ROSGAN_PUBLIC_STATS_V2__) return;
32984
32985  const AUCTION_ID = '2';
32986  const SAME_ORIGIN_BASE = '/api/cabin-auctions';
32987  const REMOTE_BASE = 'https://cabanasrosgan-3y52xraydq-uc.a.run.app/api/cabin-auctions';
32988
32989  const byId = (id) => document.getElementById(id);
32990  const nf = new Intl.NumberFormat('es-AR');
32991
32992  function escapeHtml(value){
32993    return String(value ?? '')
32994      .replace(/&/g, '&amp;')
32995      .replace(/</g, '&lt;')
32996      .replace(/>/g, '&gt;')
32997      .replace(/"/g, '&quot;')
32998      .replace(/'/g, '&#039;');
32999  }
33000
33001  function normalizeArray(payload){
33002    if (Array.isArray(payload)) return payload;
33003    if (Array.isArray(payload?.items)) return payload.items;
33004    if (Array.isArray(payload?.data)) return payload.data;
33005    if (Array.isArray(payload?.results)) return payload.results;
33006    if (Array.isArray(payload?.$values)) return payload.$values;
33007    return [];
33008  }
33009
33010  async function fetchJson(path){
33011    const targets = [SAME_ORIGIN_BASE + path, REMOTE_BASE + path];
33012    let lastError = null;
33013    for (const url of targets){
33014      try{
33015        const controller = new AbortController();
33016        const timer = setTimeout(() => controller.abort(), 12000);
33017        const response = await fetch(url, {
33018          headers:{ accept:'application/json' },
33019          signal:controller.signal
33020        });
33021        clearTimeout(timer);
33022        if (!response.ok) throw new Error(response.status + ' ' + response.statusText);
33023        return await response.json();
33024      }catch(error){
33025        lastError = error;
33026      }
33027    }
33028    throw lastError || new Error('No se pudo consultar el endpoint.');
33029  }
33030
33031  function primaryCabana(auction){
33032    const principales = normalizeArray(auction?.cabanasPrincipales);
33033    return principales[0] || normalizeArray(auction?.cabanas || auction?.cabins)[0] || null;
33034  }
33035
33036  function lotItems(lot){
33037    const items = normalizeArray(lot?.items);
33038    return items.length ? items : [{ values:lot?.values || {}, displayOrder:lot?.displayOrder || lot?.order || 0 }];
33039  }
33040
33041  function firstValues(lot){
33042    return lotItems(lot)[0]?.values || {};
33043  }
33044
33045  function fieldValue(values, keys){
33046    for (const key of keys){
33047      const value = values?.[key];
33048      if (value !== undefined && value !== null && value !== '') return value;
33049    }
33050    return '';
33051  }
33052
33053  function groupBy(list, getKey){
33054    const map = new Map();
33055    for (const item of list){
33056      const key = getKey(item) || 'Sin dato';
33057      map.set(key, (map.get(key) || 0) + 1);
33058    }
33059    return [...map.entries()]
33060      .map(([label,count]) => ({ label, count }))
33061      .sort((a,b) => b.count - a.count || a.label.localeCompare(b.label));
33062  }
33063
33064  function sumItems(lots){
33065    return lots.reduce((total, lot) => total + Math.max(1, lotItems(lot).length), 0);
33066  }
33067
33068  function completionForLot(lot){
33069    const values = firstValues(lot);
33070    const entries = Object.values(values || {});
33071    if (!entries.length) return 0;
33072    const filled = entries.filter((value) => value !== null && value !== undefined && value !== '').length;
33073    return filled / entries.length;
33074  }
33075
33076  function buildSummary(auction, lots){
33077    const rpTotal = sumItems(lots);
33078    const withCover = lots.filter((lot) => !!(lot.coverImage?.url || lot.coverImageUrl || lot.imageUrl)).length;
33079    const avgCompletion = lots.length
33080      ? lots.reduce((sum, lot) => sum + completionForLot(lot), 0) / lots.length
33081      : 0;
33082    const categories = groupBy(lots, (lot) => lot.categoryName || firstValues(lot).categoria || firstValues(lot).categoryName);
33083    const cabanas = groupBy(lots, (lot) => lot.cabanaName || firstValues(lot).cabana || firstValues(lot).cabanaName);
33084    return { rpTotal, withCover, avgCompletion, categories, cabanas };
33085  }
33086
33087  function renderKpis(summary, lots){
33088    const categoryCount = summary.categories.length;
33089    const cabanaCount = summary.cabanas.filter((item) => item.label !== 'Sin dato').length;
33090    byId('lc-stats-kpis').innerHTML = [
33091      ['Lotes', nf.format(lots.length), nf.format(summary.rpTotal) + ' RP cargados'],
33092      ['Toros / RP', nf.format(summary.rpTotal), 'Cabezas individuales del catalogo'],
33093      ['Categorias', nf.format(categoryCount), categoryCount === 1 ? 'Categoria disponible' : 'Categorias disponibles'],
33094      ['Cabanas', nf.format(cabanaCount), 'Participantes con lotes']
33095    ].map(([label,value,detail]) => `
33096      <article class="lc-stats-kpi">
33097        <span>${escapeHtml(label)}</span>
33098        <strong>${escapeHtml(value)}</strong>
33099        <em>${escapeHtml(detail)}</em>
33100      </article>
33101    `).join('');
33102  }
33103
33104  function renderBars(targetId, groups, total){
33105    const safeTotal = Math.max(1, total);
33106    const html = groups.length ? groups.map((item) => {
33107      const pct = Math.round(item.count / safeTotal * 100);
33108      return `
33109        <div class="lc-stat-row">
33110          <b>${escapeHtml(item.label)}</b>
33111          <div class="lc-stat-track"><div class="lc-stat-fill" style="width:${pct}%"></div></div>
33112          <strong>${escapeHtml(nf.format(item.count))} · ${pct}%</strong>
33113        </div>
33114      `;
33115    }).join('') : '<div class="lc-stats-mini"><b>Sin datos cargados</b><span>--</span></div>';
33116    byId(targetId).innerHTML = html;
33117  }
33118
33119  function renderCabanaList(summary, total){
33120    const rows = summary.cabanas.length ? summary.cabanas.map((item) => {
33121      const pct = Math.round(item.count / Math.max(1,total) * 100);
33122      return `<div class="lc-stats-mini"><b>${escapeHtml(item.label)}</b><span>${escapeHtml(nf.format(item.count))} lotes · ${pct}%</span></div>`;
33123    }).join('') : '<div class="lc-stats-mini"><b>Sin cabanas asociadas</b><span>--</span></div>';
33124    byId('lc-stats-cabana-list').innerHTML = rows;
33125  }
33126
33127  function renderQuality(summary, lots){
33128    const total = Math.max(1, lots.length);
33129    const rows = [
33130      { label:'Lotes con imagen principal', count:summary.withCover },
33131      { label:'Completitud promedio de datos', count:Math.round(summary.avgCompletion * 100), percentMode:true }
33132    ];
33133    byId('lc-stats-quality-bars').innerHTML = rows.map((item) => {
33134      const pct = item.percentMode ? item.count : Math.round(item.count / total * 100);
33135      const label = item.percentMode ? pct + '%' : nf.format(item.count) + ' · ' + pct + '%';
33136      return `
33137        <div class="lc-stat-row">
33138          <b>${escapeHtml(item.label)}</b>
33139          <div class="lc-stat-track"><div class="lc-stat-fill" style="width:${Math.max(2, pct)}%"></div></div>
33140          <strong>${escapeHtml(label)}</strong>
33141        </div>
33142      `;
33143    }).join('');
33144  }
33145
33146  function renderTopLots(lots){
33147    const rows = lots
33148      .slice()
33149      .sort((a,b) => Number(a.displayOrder || 9999) - Number(b.displayOrder || 9999))
33150      .slice(0, 10)
33151      .map((lot, index) => {
33152        const values = firstValues(lot);
33153        const rp = fieldValue(values, ['rp','RP','caravana','lote']) || lot.lotCert || lot.id || '-';
33154        return `
33155          <tr>
33156            <td>${escapeHtml(lot.displayOrder || index + 1)}</td>
33157            <td>${escapeHtml(fieldValue(values, ['lote','Lote']) || lot.lotCert || lot.id || '-')}</td>
33158            <td>${escapeHtml(rp)}</td>
33159            <td>${escapeHtml(lot.categoryName || '-')}</td>
33160            <td>${escapeHtml(lot.cabanaName || '-')}</td>
33161          </tr>
33162        `;
33163      }).join('');
33164    byId('lc-stats-top-lots').innerHTML = rows || '<tr><td colspan="5">No hay lotes cargados para este remate.</td></tr>';
33165  }
33166
33167  function renderMedia(summary, lots, videoInfo){
33168    const total = Math.max(1, lots.length);
33169    const videoLots = videoInfo?.lotsWithVideos ?? null;
33170    const videoText = videoLots === null
33171      ? 'No disponible'
33172      : nf.format(videoLots) + ' lotes · ' + Math.round(videoLots / total * 100) + '%';
33173    byId('lc-stats-media-list').innerHTML = `
33174      <div class="lc-stats-mini"><b>Imagen principal</b><span>${escapeHtml(nf.format(summary.withCover))} lotes · ${Math.round(summary.withCover / total * 100)}%</span></div>
33175      <div class="lc-stats-mini"><b>Videos cargados</b><span>${escapeHtml(videoText)}</span></div>
33176      <div class="lc-stats-mini"><b>Total de videos</b><span>${escapeHtml(videoInfo?.totalVideos ?? 'No disponible')}</span></div>
33177    `;
33178  }
33179
33180  async function getVideoInfo(lots){
33181    const lotIds = lots.map((lot) => lot.id || lot.lotCert || lot.lotCertificado).filter(Boolean);
33182    if (!lotIds.length) return { lotsWithVideos:null, totalVideos:'No disponible' };
33183    const limited = lotIds.slice(0, 80);
33184    const results = await Promise.allSettled(limited.map((id) => fetchJson(`/${AUCTION_ID}/lots/${encodeURIComponent(id)}/videos`)));
33185    let totalVideos = 0;
33186    let lotsWithVideos = 0;
33187    let ok = 0;
33188    results.forEach((result) => {
33189      if (result.status !== 'fulfilled') return;
33190      ok += 1;
33191      const videos = normalizeArray(result.value);
33192      totalVideos += videos.length;
33193      if (videos.length) lotsWithVideos += 1;
33194    });
33195    if (!ok) return { lotsWithVideos:null, totalVideos:'No disponible' };
33196    return { lotsWithVideos, totalVideos:nf.format(totalVideos) };
33197  }
33198
33199  function renderLogo(auction){
33200    const cabana = primaryCabana(auction);
33201    const logo = window.resolveCabanaMediaUrl(cabana?.logoUrl || cabana?.logoDataUrl || cabana?.whiteLogoUrl || auction?.logoUrl);
33202    const name = cabana?.nombre || cabana?.name || 'La Cassina';
33203    byId('lc-stats-logo-card').innerHTML = logo
33204      ? `<img src="${escapeHtml(logo)}" alt="${escapeHtml(name)}">`
33205      : `<strong style="color:#0b315d;font-size:32px">${escapeHtml(name)}</strong>`;
33206  }
33207
33208  function renderAuction(auction, lots){
33209    const summary = buildSummary(auction, lots);
33210    const title = auction?.title || auction?.nombre || 'Remate La Cassina';
33211    byId('lc-stats-auction-title').textContent = title + ' · Tablero publico de actividad del catalogo.';
33212    byId('lc-stats-status').textContent = 'Datos actualizados';
33213    renderLogo(auction);
33214    renderKpis(summary, lots);
33215    renderBars('lc-stats-category-bars', summary.categories, lots.length);
33216    renderCabanaList(summary, lots.length);
33217    renderQuality(summary, lots);
33218    renderTopLots(lots);
33219    renderMedia(summary, lots, null);
33220    getVideoInfo(lots).then((videoInfo) => renderMedia(summary, lots, videoInfo)).catch(() => renderMedia(summary, lots, null));
33221  }
33222
33223  function renderError(error){
33224    byId('lc-stats-status').textContent = 'Error de carga';
33225    byId('lc-stats-kpis').innerHTML = `<div class="lc-stats-error">No pudimos cargar las estadisticas del remate. ${escapeHtml(error?.message || '')}</div>`;
33226    byId('lc-stats-category-bars').innerHTML = '';
33227    byId('lc-stats-cabana-list').innerHTML = '';
33228    byId('lc-stats-quality-bars').innerHTML = '';
33229    byId('lc-stats-media-list').innerHTML = '';
33230    byId('lc-stats-top-lots').innerHTML = '<tr><td colspan="5">Sin datos disponibles.</td></tr>';
33231  }
33232
33233  async function init(){
33234    try{
33235      const [auction, lotsPayload] = await Promise.all([
33236        fetchJson(`/${AUCTION_ID}`),
33237        fetchJson(`/${AUCTION_ID}/lots/`)
33238      ]);
33239      renderAuction(auction, normalizeArray(lotsPayload));
33240    }catch(error){
33241      renderError(error);
33242    }
33243  }
33244
33245  if (document.readyState === 'loading'){
33246    document.addEventListener('DOMContentLoaded', init, { once:true });
33247  }else{
33248    init();
33249  }
33250})();
33251</script>
vendor: 1 bytes, line 33251
33251
33252<script id="lacassina-public-stats-v2-script">
33253(function(){
33254  if (window.__ROSGAN_PUBLIC_ROUTE__ !== 'lacassina-stats') return;
33255
33256  const AUCTION_ID = '2';
33257  const CABIN_BASES = ['/api/cabin-auctions', 'https://cabanasrosgan-3y52xraydq-uc.a.run.app/api/cabin-auctions'];
33258  const ANALYTICS_BASES = [
33259    '/api/cabanas-analytics',
33260    'https://us-central1-rosganapp.cloudfunctions.net/cabanasAnalytics',
33261    'https://us-central1-rosganapp.cloudfunctions.net/cabanasAnalytics/api/cabanas-analytics'
33262  ];
33263  const STATS_REFRESH_MS = 7000;
33264  const nf = new Intl.NumberFormat('es-AR');
33265  const byId = (id) => document.getElementById(id);
33266  const clean = (value) => String(value ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;');
33267  const consKey = (value) => String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '');
33268  const normalize = (payload) => Array.isArray(payload) ? payload : Array.isArray(payload?.items) ? payload.items : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.results) ? payload.results : Array.isArray(payload?.$values) ? payload.$values : [];
33269  const hasValue = (value) => value !== undefined && value !== null && String(value).trim() !== '' && String(value).trim() !== '-' && String(value).trim() !== '--';
33270  const sum = (items, key) => items.reduce((acc, item) => acc + Number(item[key] || 0), 0);
33271  const pct = (value, total) => Math.round((Number(value || 0) / Math.max(1, Number(total || 0))) * 100);
33272
33273  function noCachePath(path){
33274    const separator = String(path).includes('?') ? '&' : '?';
33275    return `${path}${separator}_=${Date.now()}`;
33276  }
33277
33278  async function fetchFromBases(bases, path, options = {}){
33279    let lastError = null;
33280    for (const base of bases){
33281      let timer = null;
33282      try {
33283        const controller = new AbortController();
33284        timer = setTimeout(() => controller.abort(), 14000);
33285        const response = await fetch(`${base}${options.noCache ? noCachePath(path) : path}`, { headers: { accept: 'application/json' }, signal: controller.signal, cache: 'no-store' });
33286        if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
33287        return await response.json();
33288      } catch (error) {
33289        lastError = error;
33290      } finally {
33291        if (timer) clearTimeout(timer);
33292      }
33293    }
33294    throw lastError || new Error('No se pudo consultar el endpoint.');
33295  }
33296
33297  function parseMetric(value){
33298    if (typeof value === 'number' && Number.isFinite(value)) return value;
33299    if (typeof value === 'boolean') return value ? 1 : 0;
33300    if (!hasValue(value)) return null;
33301    const parsed = Number(String(value).replace(/\./g,'').replace(',','.').replace(/[^\d.+-]/g,''));
33302    return Number.isFinite(parsed) ? parsed : null;
33303  }
33304
33305  function nestedMetric(row, key){
33306    const sources = [row, row?.metrics, row?.totals, row?.counters, row?.data, row?.summary, row?.analytics];
33307    for (const source of sources){
33308      if (!source || typeof source !== 'object') continue;
33309      const parsed = parseMetric(source[key]);
33310      if (parsed !== null) return parsed;
33311    }
33312    return null;
33313  }
33314
33315  function metric(row, keys){
33316    for (const key of keys){
33317      const parsed = nestedMetric(row, key);
33318      if (parsed !== null) return parsed;
33319    }
33320    return 0;
33321  }
33322
33323  function dataRoot(payload){
33324    const root = payload?.data || payload?.result || payload?.payload || payload || {};
33325    return root?.data || root;
33326  }
33327
33328  function firstArray(data, keys){
33329    for (const key of keys){
33330      const value = data?.[key];
33331      const arr = normalize(value);
33332      if (arr.length) return arr;
33333      const nested = normalize(value?.items || value?.data || value?.rows);
33334      if (nested.length) return nested;
33335    }
33336    return [];
33337  }
33338
33339  function firstObject(data, keys){
33340    for (const key of keys){
33341      const value = data?.[key];
33342      if (value && typeof value === 'object' && !Array.isArray(value)) return value;
33343    }
33344    return {};
33345  }
33346
33347  function totalFrom(summary, keys){
33348    return metric(summary?.totals, keys) || metric(summary?.auction, keys) || metric(summary?.summary, keys);
33349  }
33350
33351  function displayMetric(value, state){
33352    const number = Number(value || 0);
33353    if (!state?.ok && !number) return '--';
33354    return nf.format(number);
33355  }
33356
33357  function analyticsTotals(rows, summary){
33358    const whatsappClicks = sum(rows, 'whatsappClicks') || totalFrom(summary, ['whatsappClicks','whatsapp','whatsAppClicks','whatsapp_clicks','contactWhatsapp']);
33359    const callClicks = sum(rows, 'callClicks') || totalFrom(summary, ['callClicks','calls','phoneClicks','call_clicks']);
33360    const contactClicks = sum(rows, 'contactClicks') || totalFrom(summary, ['contactClicks','contacts','contactCount']);
33361    const contacts = Math.max(contactClicks, whatsappClicks + callClicks);
33362    const shares = sum(rows, 'shares') || totalFrom(summary, ['shares','shareClicks','sharedCount','event_share_click']);
33363    const durationMs = rows.reduce((acc, row) => acc + row.durationMs, 0) || totalFrom(summary, ['viewDurationMsTotal','durationMsTotal','totalDurationMs','viewTimeMs','totalViewTimeMs']);
33364    const durationSamples = rows.reduce((acc, row) => acc + row.durationSamples, 0) || totalFrom(summary, ['viewDurationSamples','durationSamples','samples','durationCount']);
33365    return {
33366      lotViews: sum(rows, 'lotViews') || totalFrom(summary, ['lotViews','views','viewCount','totalViews','lot_view','lotViewsTotal']),
33367      rpViews: sum(rows, 'rpViews') || totalFrom(summary, ['rpViews','rpViewCount','rp_views','totalRpViews']),
33368      videoPlays: sum(rows, 'videoPlays') || totalFrom(summary, ['videoPlays','plays','playCount','totalPlays','video_play']),
33369      imageViews: sum(rows, 'imageViews') || totalFrom(summary, ['imageViews','imageViewCount','images','totalImageViews']),
33370      whatsappClicks,
33371      callClicks,
33372      contactClicks,
33373      contacts,
33374      shares,
33375      durationMs,
33376      durationSamples
33377    };
33378  }
33379
33380  function summaryEventCount(summary){
33381    return totalFrom(summary, ['events','eventCount','totalEvents','signals','count'])
33382      || summary.lots.length + summary.rps.length + summary.videos.length + summary.images.length + summary.daily.length;
33383  }
33384
33385  function durationLabel(ms){
33386    const total = Math.round(Number(ms || 0) / 1000);
33387    if (!total) return 'Sin datos';
33388    const min = Math.floor(total / 60);
33389    const sec = total % 60;
33390    return min ? `${min}m ${String(sec).padStart(2,'0')}s` : `${sec}s`;
33391  }
33392
33393  function empty(id, title, text = ''){
33394    const node = byId(id);
33395    if (!node) return;
33396    node.innerHTML = `<div class="lc-stats-empty"><strong>${clean(title)}</strong>${text ? `<span>${clean(text)}</span>` : ''}</div>`;
33397  }
33398
33399  function itemRows(lot){
33400    const items = normalize(lot?.items);
33401    return items.length ? items : [{ id: lot?.id, displayOrder: lot?.displayOrder || 0, values: lot?.values || {}, subforms: [] }];
33402  }
33403
33404  function valueOf(obj, keys){
33405    for (const key of keys){
33406      if (hasValue(obj?.[key])) return obj[key];
33407    }
33408    return '';
33409  }
33410
33411  function lotMetaEntries(lots){
33412    const entries = [];
33413    lots.forEach((lot) => {
33414      itemRows(lot).forEach((item) => {
33415        const values = item?.values || {};
33416        const cert = String(valueOf(lot, ['lotCert','cert']) || valueOf(values, ['lote','lot']) || item?.id || lot?.id || '').trim();
33417        const lotNumber = String(valueOf(values, ['lote','numero_lote','lotNumber','lot']) || valueOf(lot, ['lotNumber','numero_lote','lotCert','cert']) || item?.id || lot?.id || '').trim();
33418        const rp = String(valueOf(values, ['rp','RP','caravana']) || cert).trim();
33419        entries.push({
33420          cert,
33421          lotNumber,
33422          rp,
33423          lot,
33424          item,
33425          title: [lot?.categoryName, rp ? `RP ${rp}` : ''].filter(Boolean).join(' · ') || lot?.categoryName || `Lote ${cert}`,
33426          category: lot?.categoryName || 'Sin categoría',
33427          cabana: lot?.cabanaName || 'Sin cabaña',
33428          cover: window.resolveCabanaMediaUrl(lot?.coverImage?.url || lot?.coverImageUrl)
33429        });
33430      });
33431    });
33432    return entries;
33433  }
33434
33435  function buildMetaMap(lots){
33436    const map = new Map();
33437    lotMetaEntries(lots).forEach((meta) => {
33438      [meta.cert, meta.rp, meta.lot?.id, meta.item?.id].filter(hasValue).forEach((key) => map.set(String(key), meta));
33439    });
33440    return map;
33441  }
33442
33443  function matchMeta(row, map){
33444    const keys = [row?.lotCert, row?.cert, row?.lotId, row?.lotKey, row?.id, row?.rp, row?.rpKey, row?.itemId, row?.itemKey].filter(hasValue).map(String);
33445    for (const key of keys){
33446      if (map.has(key)) return map.get(key);
33447    }
33448    return null;
33449  }
33450
33451  function normalizeSummary(payload){
33452    const data = payload?.data || payload || {};
33453    return {
33454      ok: data.ok !== false,
33455      lots: normalize(data.lots),
33456      rps: normalize(data.rps),
33457      videos: normalize(data.videos),
33458      images: normalize(data.images),
33459      daily: normalize(data.daily),
33460      hourly: normalize(data.hourly),
33461      source: data.source || '',
33462      totals: data.totals || null,
33463      summary: data.summary || null,
33464      auction: data.auction || null
33465    };
33466  }
33467
33468  function aggregate(summary, lots, map){
33469    const rows = new Map();
33470    const ensure = (key, meta = {}) => {
33471      const id = String(key || '').trim();
33472      if (!id) return null;
33473      if (!rows.has(id)){
33474        rows.set(id, {
33475          key: id,
33476          cert: meta.cert || id,
33477          lotNumber: meta.lotNumber || meta.cert || id,
33478          rp: meta.rp || '',
33479          title: meta.title || `Lote ${id}`,
33480          category: meta.category || 'Sin categoría',
33481          cabana: meta.cabana || 'Sin cabaña',
33482          lotViews: 0,
33483          rpViews: 0,
33484          videoPlays: 0,
33485          imageViews: 0,
33486          whatsappClicks: 0,
33487          callClicks: 0,
33488          contactClicks: 0,
33489          shares: 0,
33490          durationMs: 0,
33491          durationSamples: 0
33492        });
33493      }
33494      return rows.get(id);
33495    };
33496
33497    lotMetaEntries(lots).forEach((meta) => ensure(meta.cert || meta.rp || meta.lot?.id, meta));
33498
33499    const apply = (src, type) => {
33500      const meta = matchMeta(src, map) || {};
33501      const row = ensure(meta.cert || src.lotCert || src.cert || src.lotId || src.id || src.rp || src.itemId, meta);
33502      if (!row) return;
33503      Object.assign(row, {
33504        title: meta.title || row.title,
33505        category: meta.category || row.category,
33506        cabana: meta.cabana || row.cabana,
33507        rp: meta.rp || row.rp,
33508        cert: meta.cert || row.cert,
33509        lotNumber: meta.lotNumber || row.lotNumber || meta.cert || row.cert
33510      });
33511      if (type === 'lot') {
33512        row.lotViews += metric(src, ['lotViews','views','viewCount']);
33513        row.whatsappClicks += metric(src, ['whatsappClicks','whatsapp','whatsAppClicks']);
33514        row.callClicks += metric(src, ['callClicks','calls']);
33515        row.contactClicks += metric(src, ['contactClicks','contacts']);
33516      } else if (type === 'rp') {
33517        row.rpViews += metric(src, ['rpViews','views','viewCount']);
33518        row.shares += metric(src, ['shares','shareClicks','sharedCount','event_share_click']);
33519        row.durationMs += metric(src, ['viewDurationMsTotal','durationMsTotal','totalDurationMs','viewTimeMs']);
33520        row.durationSamples += metric(src, ['viewDurationSamples','durationSamples','samples']);
33521      } else if (type === 'video') {
33522        row.videoPlays += metric(src, ['videoPlays','plays','playCount','count']);
33523      } else if (type === 'image') {
33524        row.imageViews += metric(src, ['imageViews','views','viewCount','count']);
33525      }
33526    };
33527
33528    summary.lots.forEach((x) => apply(x, 'lot'));
33529    summary.rps.forEach((x) => apply(x, 'rp'));
33530    summary.videos.forEach((x) => apply(x, 'video'));
33531    summary.images.forEach((x) => apply(x, 'image'));
33532    return Array.from(rows.values());
33533  }
33534
33535  function renderHeader(auction, lots, summary, state){
33536    const title = auction?.title || summary.auction?.title || 'La Cassina';
33537    const cabins = normalize(auction?.cabins || auction?.cabanas);
33538    const configuredPrincipals = normalize(auction?.cabanasPrincipales);
33539    const configuredGuests = normalize(auction?.cabanasInvitadas);
33540    const principals = configuredPrincipals.length ? configuredPrincipals : cabins.filter((cabana) => String(cabana?.type || '').toLowerCase() === 'principal');
33541    const guests = configuredGuests.length ? configuredGuests : cabins.filter((cabana) => String(cabana?.type || '').toLowerCase() !== 'principal');
33542    const principal = principals[0] || cabins[0] || {};
33543    const logoFor = (cabana) => window.resolveCabanaMediaUrl(cabana?.logoUrl || cabana?.logoDataUrl || cabana?.logo || cabana?.imageUrl);
33544    const nameFor = (cabana) => cabana?.nombre || cabana?.name || cabana?.title || 'Cabana';
33545    const logo = logoFor(principal);
33546    const titleNode = byId('lc-stats-auction-title');
33547    if (titleNode) titleNode.textContent = title;
33548    const logoNode = byId('lc-stats-logo-card');
33549    if (logoNode) {
33550      const guestLogos = guests
33551        .filter((cabana) => nameFor(cabana) || logoFor(cabana))
33552        .slice(0, 6)
33553        .map((cabana) => {
33554          const guestLogo = logoFor(cabana);
33555          const guestName = nameFor(cabana);
33556          return `<div class="lc-stats-guest-logo">${guestLogo ? `<img src="${clean(guestLogo)}" alt="${clean(guestName)}">` : `<span>${clean(guestName)}</span>`}</div>`;
33557        }).join('');
33558      logoNode.innerHTML = `
33559        <span class="lc-stats-logo-label">Cabana principal</span>
33560        ${logo ? `<img class="lc-stats-main-logo" src="${clean(logo)}" alt="${clean(nameFor(principal))}">` : `<strong>${clean(nameFor(principal) || title)}</strong>`}
33561        ${guestLogos ? `<div class="lc-stats-guest-logos">${guestLogos}</div>` : ''}
33562      `;
33563    }
33564    const eventCount = summaryEventCount(summary);
33565    const statusNode = byId('lc-stats-status');
33566    const updated = state.updatedAt ? new Intl.DateTimeFormat('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(state.updatedAt) : '';
33567    if (statusNode) statusNode.innerHTML = `<span>${nf.format(lots.length)} lotes publicados</span><strong>${state.ok ? `${nf.format(eventCount)} señales registradas` : 'Analytics sin respuesta'}</strong>${updated ? `<small>Actualizado ${clean(updated)}</small>` : ''}${state.error ? `<small>${clean(state.error)}</small>` : ''}`;
33568  }
33569
33570  function renderKpis(rows, state, summary){
33571    const totals = analyticsTotals(rows, summary);
33572    const lotViews = totals.lotViews;
33573    const rpViews = totals.rpViews;
33574    const videoPlays = totals.videoPlays;
33575    const imageViews = totals.imageViews;
33576    const contacts = totals.contacts;
33577    const cards = [
33578      ['Visitas de lotes', lotViews, state.ok ? 'Aperturas de ficha y catálogo' : 'Esperando analytics'],
33579      ['Vistas por RP', rpViews, 'Interacciones con reproductores'],
33580      ['Plays de video', videoPlays, 'Reproducciones registradas'],
33581      ['Vistas de imagen', imageViews, 'Galería y portadas'],
33582      ['Compartidos', totals.shares, 'Links compartidos o copiados'],
33583      ['Tiempo prom. RP', durationLabel(totals.durationSamples ? totals.durationMs / totals.durationSamples : 0), totals.durationSamples ? `${nf.format(totals.durationSamples)} visitas medidas` : 'Sin muestras'],
33584      ['Contactos', contacts, 'WhatsApp, llamada y contacto']
33585    ];
33586    const node = byId('lc-stats-kpis');
33587    if (node) node.innerHTML = cards.map(([label, value, hint]) => `<article class="lc-stat-card"><span>${clean(label)}</span><strong>${typeof value === 'number' ? nf.format(value) : clean(value)}</strong><small>${clean(hint)}</small></article>`).join('');
33588  }
33589
33590  function barRows(id, rows){
33591    const node = byId(id);
33592    if (!node) return;
33593    const filtered = rows.filter(([, value]) => Number(value) > 0);
33594    if (!filtered.length){
33595      empty(id, 'Sin señales todavía', 'No hay eventos suficientes para este módulo.');
33596      return;
33597    }
33598    const max = Math.max(1, ...filtered.map(([, value]) => Number(value)));
33599    node.innerHTML = filtered.map(([label, value]) => `<div class="lc-stats-bar-row"><span>${clean(label)}</span><div><i style="width:${pct(value, max)}%"></i></div><strong>${nf.format(value)}</strong></div>`).join('');
33600  }
33601
33602  function groupBy(rows, key, valueFn, id){
33603    const map = new Map();
33604    rows.forEach((row) => {
33605      const label = row[key] || 'Sin dato';
33606      map.set(label, (map.get(label) || 0) + valueFn(row));
33607    });
33608    barRows(id, Array.from(map.entries()).sort((a, b) => b[1] - a[1]).slice(0, 8));
33609  }
33610
33611  function renderInsights(rows, lots, summary){
33612    const totals = analyticsTotals(rows, summary);
33613    const totalViews = totals.lotViews + totals.rpViews;
33614    const contacts = totals.contacts;
33615    const active = rows.filter((row) => row.lotViews || row.rpViews || row.videoPlays || row.imageViews || row.shares).length;
33616    const top = active ? rows.slice().sort((a, b) => (b.lotViews + b.rpViews + b.videoPlays + b.shares) - (a.lotViews + a.rpViews + a.videoPlays + a.shares))[0] : null;
33617    const topLotLabel = top ? (top.lotNumber || top.cert || top.lotId || '').trim() : '';
33618    const topRpLabel = top?.rp ? `RP ${top.rp}` : '';
33619    const topCategory = top?.category || top?.title || '';
33620    const topDetail = [topRpLabel, topCategory].filter(Boolean).join(' - ');
33621    const insights = byId('lc-stats-insights');
33622    if (insights) insights.innerHTML = [
33623      ['Lote destacado', topLotLabel ? `Lote ${topLotLabel}` : 'Sin lote lider aun', topDetail || 'Se completa con las primeras senales'],
33624      ['Cobertura activa', `${nf.format(active)} / ${nf.format(lots.length)}`, 'Lotes con visitas, videos o imagenes'],
33625      ['Contactos', contacts ? nf.format(contacts) : 'Sin datos', contacts ? 'Consultas registradas desde el catalogo' : 'Esperando WhatsApp o llamadas']
33626    ].map(([label, value, detail]) => `<article class="lc-stats-insight"><span>${clean(label)}</span><strong>${clean(value)}</strong><em>${clean(detail)}</em></article>`).join('');
33627  }
33628
33629  function renderFunnelCounts(rows, summary){
33630    const totals = analyticsTotals(rows, summary);
33631    const cards = [
33632      ['Vistas lote', totals.lotViews, 'Aperturas de ficha'],
33633      ['RPs vistos', totals.rpViews, 'RPs efectivamente abiertos'],
33634      ['Videos', totals.videoPlays, 'Play real del reproductor'],
33635      ['Compartidos', totals.shares, 'Links enviados o copiados'],
33636      ['Contactos', totals.contacts, 'WhatsApp y llamadas']
33637    ];
33638    const node = byId('lc-stats-funnel');
33639    if (node) node.innerHTML = cards.map(([label, value, hint]) => `<article class="lc-stats-count-card"><span>${clean(label)}</span><strong>${nf.format(value)}</strong><small>${clean(hint)}</small></article>`).join('');
33640  }
33641
33642  function timelineValue(row){
33643    return metric(row, ['lotViews']) + metric(row, ['rpViews']) + metric(row, ['videoPlays']) + metric(row, ['imageViews']) + metric(row, ['contactClicks']) + metric(row, ['shares','shareClicks','event_share_click']);
33644  }
33645
33646  function timelineLabel(key, mode){
33647    const text = String(key || '');
33648    if (mode === 'semana') return text.slice(5, 10).replace('-', '/');
33649    const hour = text.match(/T(\d{2})/)?.[1];
33650    return hour ? `${hour}h` : text.slice(5, 10).replace('-', '/');
33651  }
33652
33653  function renderTimeline(summary, mode = window.__lcStatsRange || 'dia'){
33654    window.__lcStatsRange = mode;
33655    const tabs = byId('lc-stats-range-tabs');
33656    if (tabs) {
33657      const options = [['dia', 'Dia'], ['semana', 'Semana'], ['horas', 'Ultimas horas']];
33658      tabs.innerHTML = options.map(([key, label]) => `<button type="button" class="${key === mode ? 'is-active' : ''}" data-lc-range="${key}">${label}</button>`).join('');
33659      tabs.querySelectorAll('button').forEach((button) => {
33660        button.addEventListener('click', () => renderTimeline(summary, button.dataset.lcRange));
33661      });
33662    }
33663    const hourly = normalize(summary.hourly);
33664    const daily = normalize(summary.daily);
33665    let rows = [];
33666    if (mode === 'semana') rows = daily.slice(-7).map((row) => ({ key: row.dayKey || row.date || row.day, row }));
33667    else rows = (hourly.length ? hourly : daily).slice(mode === 'horas' ? -12 : -24).map((row) => ({ key: row.hourKey || row.dayKey || row.date || row.day, row }));
33668    rows = rows.filter((item) => item.key);
33669    const node = byId('lc-stats-timeline');
33670    if (!node) return;
33671    if (!rows.length){
33672      empty('lc-stats-timeline', 'Sin serie temporal todavia', 'Los eventos nuevos ya van a guardarse por hora y por dia.');
33673      return;
33674    }
33675    const values = rows.map((item) => timelineValue(item.row));
33676    const max = Math.max(1, ...values);
33677    const width = 640;
33678    const height = 240;
33679    const padX = 42;
33680    const padTop = 32;
33681    const padBottom = 44;
33682    const plotW = width - padX * 2;
33683    const plotH = height - padTop - padBottom;
33684    const points = values.map((value, index) => {
33685      const x = rows.length === 1 ? width / 2 : padX + (plotW * 
33685index / Math.max(1, rows.length - 1));
33686      const y = padTop + plotH - (plotH * (value / max));
33687      return { x, y, value, label: timelineLabel(rows[index].key, mode) };
33688    });
33689    const linePath = points.map((point, index) => `${index ? 'L' : 'M'} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' ');
33690    const areaPath = points.length
33691      ? `${linePath} L ${points[points.length - 1].x.toFixed(1)} ${height - padBottom} L ${points[0].x.toFixed(1)} ${height - padBottom} Z`
33692      : '';
33693    const totals = rows.reduce((acc, item) => {
33694      acc.events += timelineValue(item.row);
33695      acc.views += metric(item.row, ['lotViews']) + metric(item.row, ['rpViews']);
33696      acc.videos += metric(item.row, ['videoPlays']);
33697      acc.contacts += metric(item.row, ['contactClicks']);
33698      acc.shares += metric(item.row, ['shares','shareClicks','event_share_click']);
33699      return acc;
33700    }, { events: 0, views: 0, videos: 0, contacts: 0, shares: 0 });
33701    node.innerHTML = `
33702      <div class="lc-stats-chart">
33703        <svg class="lc-stats-line-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="Evolucion de señales">
33704          <defs>
33705            <linearGradient id="lcStatsLineArea" x1="0" x2="0" y1="0" y2="1">
33706              <stop offset="0%" stop-color="#18b8ad" stop-opacity=".30"></stop>
33707              <stop offset="100%" stop-color="#0b67b2" stop-opacity=".04"></stop>
33708            </linearGradient>
33709          </defs>
33710          <line class="lc-stats-line-grid" x1="${padX}" x2="${width - padX}" y1="${padTop}" y2="${padTop}"></line>
33711          <line class="lc-stats-line-grid" x1="${padX}" x2="${width - padX}" y1="${padTop + plotH / 2}" y2="${padTop + plotH / 2}"></line>
33712          <line class="lc-stats-line-grid" x1="${padX}" x2="${width - padX}" y1="${height - padBottom}" y2="${height - padBottom}"></line>
33713          ${areaPath ? `<path class="lc-stats-line-area" d="${areaPath}"></path>` : ''}
33714          ${linePath ? `<path class="lc-stats-line-path" d="${linePath}"></path>` : ''}
33715          ${points.map((point) => `<g><circle class="lc-stats-line-dot" cx="${point.x.toFixed(1)}" cy="${point.y.toFixed(1)}" r="5"></circle><text class="lc-stats-line-value" x="${point.x.toFixed(1)}" y="${Math.max(16, point.y - 12).toFixed(1)}">${nf.format(point.value)}</text><text class="lc-stats-line-label" x="${point.x.toFixed(1)}" y="${height - 16}">${clean(point.label)}</text></g>`).join('')}
33716        </svg>
33717      </div>
33718      <div class="lc-stats-chart-summary">
33719        <div><span>Vistas</span><strong>${nf.format(totals.views)}</strong></div>
33720        <div><span>Videos</span><strong>${nf.format(totals.videos)}</strong></div>
33721        <div><span>Contactos</span><strong>${nf.format(totals.contacts)}</strong></div>
33722        <div><span>Compartidos</span><strong>${nf.format(totals.shares)}</strong></div>
33723      </div>
33724    `;
33725  }
33726
33727  function renderAlerts(rows, state, summary){
33728    const totals = analyticsTotals(rows, summary);
33729    const contacts = totals.contacts;
33730    const videoPlays = totals.videoPlays;
33731    const imageViews = totals.imageViews;
33732    const alerts = [];
33733    if (!state.ok) alerts.push(['warning', 'Cloud Function sin respuesta', state.error || 'El panel queda preparado hasta que el endpoint responda.']);
33734    alerts.push([contacts ? 'good' : 'info', 'Contactos', contacts ? `${nf.format(contacts)} consultas registradas.` : 'Sin clics de WhatsApp o llamada todavía.']);
33735    alerts.push([videoPlays ? 'good' : 'info', 'Videos', videoPlays ? `${nf.format(videoPlays)} reproducciones acumuladas.` : 'Sin plays registrados todavía.']);
33736    alerts.push([imageViews ? 'good' : 'info', 'Imágenes', imageViews ? `${nf.format(imageViews)} visualizaciones de imagen.` : 'Sin vistas de imagen todavía.']);
33737    const node = byId('lc-stats-alerts');
33738    if (node) node.innerHTML = alerts.map(([type, title, text]) => {
33739      const iconClass = type === 'good' ?'dc-lucide-check' : type === 'warning' ?'dc-lucide-warning' : 'dc-lucide-info';
33740      return `<div class="lc-stats-alert ${type === 'good' ? 'is-good' : ''}"><i><span class="dc-lucide ${iconClass}" aria-hidden="true"></span></i><div><strong>${clean(title)}</strong><span>${clean(text)}</span></div></div>`;
33741    }).join('');
33742  }
33743
33744  function renderCatalogQuality(lots){
33745    const entries = lotMetaEntries(lots);
33746    if (!entries.length){
33747      empty('lc-stats-quality-bars', 'Sin lotes publicados', 'La cobertura se calcula cuando existe catalogo.');
33748      return;
33749    }
33750    const hasMedia = (meta) => {
33751      const lot = meta.lot || {};
33752      const item = meta.item || {};
33753      return Boolean(
33754        meta.cover ||
33755        lot.coverImageUrl ||
33756        lot.imageUrl ||
33757        lot.thumbnailUrl ||
33758        normalize(lot.images).length ||
33759        normalize(lot.videos).length ||
33760        normalize(item.images).length ||
33761        normalize(item.videos).length
33762      );
33763    };
33764    const completion = (meta) => {
33765      const values = meta.item?.values || meta.lot?.values || {};
33766      const fields = Object.entries(values).filter(([key]) => !['id','displayOrder'].includes(key));
33767      if (!fields.length) return 0;
33768      return fields.filter(([, value]) => hasValue(value)).length / fields.length;
33769    };
33770    const mediaCount = entries.filter(hasMedia).length;
33771    const withData = entries.filter((meta) => Object.values(meta.item?.values || meta.lot?.values || {}).some(hasValue)).length;
33772    const completeness = Math.round(entries.reduce((acc, meta) => acc + completion(meta), 0) / entries.length * 100);
33773    const rows = [
33774      ['RPs con imagen/video', pct(mediaCount, entries.length), `${nf.format(mediaCount)} de ${nf.format(entries.length)}`],
33775      ['RPs con datos tecnicos', pct(withData, entries.length), `${nf.format(withData)} de ${nf.format(entries.length)}`],
33776      ['Completitud promedio', completeness, `${nf.format(completeness)}%`]
33777    ];
33778    const node = byId('lc-stats-quality-bars');
33779    if (node) node.innerHTML = rows.map(([label, value, detail]) => `<div class="lc-stats-bar-row"><span>${clean(label)}</span><div><i style="width:${Math.max(2, Math.min(100, value))}%"></i></div><strong>${clean(detail)}</strong></div>`).join('');
33780  }
33781
33782  function renderCatalogAttributes(lots){
33783    const entries = lotMetaEntries(lots);
33784    const node = byId('lc-stats-genetics-list');
33785    if (!node) return;
33786    if (!entries.length){
33787      empty('lc-stats-genetics-list', 'Sin datos de catalogo', 'No hay atributos publicados para leer.');
33788      return;
33789    }
33790    const valueOf = (meta, keys) => {
33791      const values = meta.item?.values || meta.lot?.values || {};
33792      for (const key of keys) {
33793        const hit = Object.keys(values).find((candidate) => consKey(candidate) === consKey(key));
33794        const value = values[key] ?? (hit ? values[hit] : undefined);
33795        if (hasValue(value)) return value;
33796      }
33797      return '';
33798    };
33799    const toNumber = (value) => {
33800      const number = Number(String(value || '').replace(',', '.').replace(/[^\d.-]/g, ''));
33801      return Number.isFinite(number) ? number : null;
33802    };
33803    const average = (keys) => {
33804      const values = entries.map((meta) => toNumber(valueOf(meta, keys))).filter((value) => value !== null);
33805      return values.length ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : null;
33806    };
33807    const countBy = (keys) => {
33808      const map = new Map();
33809      entries.forEach((meta) => {
33810        const value = String(valueOf(meta, keys) || '').trim();
33811        if (!value) return;
33812        map.set(value, (map.get(value) || 0) + 1);
33813      });
33814      return [...map.entries()].sort((a, b) => b[1] - a[1])[0] || null;
33815    };
33816    const topFather = countBy(['padre', 'father', 'sire']);
33817    const categories = new Set(entries.map((meta) => meta.category).filter(hasValue));
33818    const avgWeight = average(['peso_actual', 'p_a', 'peso', 'pesoPromedio']);
33819    const avgCe = average(['ce', 'circunferencia_escrotal']);
33820    const cards = [
33821      ['RP publicados', entries.length, 'Animales leidos del catalogo'],
33822      ['Peso promedio', avgWeight ? `${nf.format(avgWeight)} kg` : 'Sin dato', 'Sobre RPs con peso cargado'],
33823      ['CE promedio', avgCe ? `${nf.format(avgCe)} cm` : 'Sin dato', 'Sobre RPs con CE cargada'],
33824      ['Padre mas repetido', topFather ? topFather[0] : 'Sin dato', topFather ? `${nf.format(topFather[1])} RPs` : 'Sin padre cargado'],
33825      ['Categorias', categories.size, 'Renglones comerciales']
33826    ];
33827    node.innerHTML = `<div class="lc-stats-attribute-grid">${cards.map(([label, value, hint]) => `<article class="lc-stats-attribute-card"><span>${clean(label)}</span><strong>${typeof value === 'number' ? nf.format(value) : clean(value)}</strong><small>${clean(hint)}</small></article>`).join('')}</div>`;
33828  }
33829
33830  function renderMedia(summary, metaMap){
33831    const list = [
33832      ...summary.videos.map((row) => ['Video', row, metric(row, ['videoPlays','plays','count'])]),
33833      ...summary.images.map((row) => ['Imagen', row, metric(row, ['imageViews','views','count'])])
33834    ].filter(([, , value]) => value > 0).sort((a, b) => b[2] - a[2]).slice(0, 10);
33835    if (!list.length){
33836      const node = byId('lc-stats-media-list');
33837      if (node) node.dataset.mediaSignature = '';
33838      empty('lc-stats-media-list', 'Sin métricas multimedia', 'Todavía no hay plays o vistas de imágenes.');
33839      return;
33840    }
33841    const node = byId('lc-stats-media-list');
33842    const mediaUrl = (row, isVideo) => {
33843      const meta = matchMeta(row, metaMap) || {};
33844      const raw = row.thumbnailUrl || row.posterUrl || row.thumb || row.imageUrl || row.imageId || row.videoId || row.mediaId || '';
33845      const text = String(raw || '');
33846      if (!isVideo && meta.cover && (!text || (text.includes('firebasestorage.googleapis.com') && !text.includes('alt=media')))) return meta.cover;
33847      return window.resolveCabanaMediaUrl(text || meta.cover);
33848    };
33849    const mediaName = (row) => {
33850      const raw = row.fileName || row.lotTitle || row.rp || row.lotCert || row.videoId || row.mediaId || 'Material';
33851      return String(raw).startsWith('http') ? decodeURIComponent(String(raw).split('/').pop() || 'Material') : raw;
33852    };
33853    const mediaTarget = (row) => {
33854      const meta = matchMeta(row, metaMap) || {};
33855      const rp = row.rp || meta.rp || '';
33856      const lot = meta.lotNumber || meta.cert || row.lotCert || row.cert || '';
33857      return lot && rp ? `/lacassina/${encodeURIComponent(String(lot).toLowerCase())}/${encodeURIComponent(String(rp).toLowerCase())}` : '/lacassina';
33858    };
33859    if (node) {
33860      node.classList.add('lc-stats-media-grid');
33861      const items = list.map(([kind, row, value]) => {
33862        const isVideo = String(kind).toLowerCase() === 'video' || String(row.mediaType || '').toLowerCase() === 'video';
33863        const url = mediaUrl(row, isVideo);
33864        const label = row.rp ? `RP ${row.rp}` : (row.lotCert ? `Lote ${row.lotCert}` : mediaName(row));
33865        const thumb = url
33866          ? `<div class="lc-stats-media-thumb">${isVideo ? `<video src="${clean(url)}" muted playsinline preload="metadata"></video>` : `<img src="${clean(url)}" alt="${clean(label)}">`}</div>`
33867          : `<div class="lc-stats-media-thumb"></div>`;
33868        return {
33869          signature: [kind, url, label, row.categoryName || row.cabanaName || mediaName(row), mediaTarget(row)].join('|'),
33870          value,
33871          html: `<a class="lc-stats-media-card" href="${clean(mediaTarget(row))}">${thumb}<div class="lc-stats-media-copy"><span>${clean(kind)}</span><strong>${clean(label)}</strong><small>${clean(row.categoryName || row.cabanaName || mediaName(row))}</small></div><strong data-lc-media-value>${nf.format(value)}</strong></a>`
33872        };
33873      });
33874      const signature = items.map((item) => item.signature).join('||');
33875      if (node.dataset.mediaSignature === signature) {
33876        node.querySelectorAll('[data-lc-media-value]').forEach((valueNode, index) => {
33877          valueNode.textContent = nf.format(items[index]?.value || 0
33877);
33878        });
33879        return;
33880      }
33881      node.dataset.mediaSignature = signature;
33882      node.innerHTML = items.map((item) => item.html).join('');
33883    }
33884  }
33885
33886  function renderDaily(summary){
33887    const daily = summary.daily.slice().sort((a, b) => String(a.date || a.day || '').localeCompare(String(b.date || b.day || ''))).slice(-10);
33888    if (!daily.length){
33889      empty('lc-stats-traffic-list', 'Sin serie diaria', 'La Cloud Function todavía no devolvió datos por día.');
33890      return;
33891    }
33892    barRows('lc-stats-traffic-list', daily.map((row) => [row.date || row.day || 'Día', metric(row, ['events','count','views','total'])]));
33893  }
33894
33895  function renderTopLots(rows){
33896    const active = rows
33897      .map((row) => ({ ...row, total: row.lotViews + row.rpViews + row.videoPlays + row.imageViews + row.whatsappClicks + row.callClicks + row.contactClicks + row.shares }))
33898      .filter((row) => row.total > 0)
33899      .sort((a, b) => b.total - a.total)
33900      .slice(0, 12);
33901    if (!active.length){
33902      const node = byId('lc-stats-top-lots');
33903      if (node) node.innerHTML = '<tr><td colspan="8"><div class="lc-stats-empty"><strong>Sin ranking de lotes</strong><span>Todavía no se registraron visitas o contactos.</span></div></td></tr>';
33904      return;
33905    }
33906    const node = byId('lc-stats-top-lots');
33907    if (!node) return;
33908    node.innerHTML = active.map((row) => {
33909      const contacts = Math.max(row.contactClicks, row.whatsappClicks + row.callClicks);
33910      const avg = row.durationSamples ? row.durationMs / row.durationSamples : 0;
33911      const lotLabel = row.lotNumber || row.cert || '-';
33912      const rpLabel = row.rp ? `RP ${row.rp}` : 'RP sin dato';
33913      const durationCell = row.durationSamples
33914        ? `${clean(durationLabel(avg))}<br><small>${nf.format(row.durationSamples)} ${row.durationSamples === 1 ? 'visita medida' : 'visitas medidas'}</small>`
33915        : 'Sin datos';
33916      return `<tr><td><strong>Lote ${clean(lotLabel)}</strong><br><small>${clean(rpLabel)} - ${clean(row.cabana)}</small></td><td>${clean(row.category)}</td><td>${nf.format(row.lotViews)}</td><td>${nf.format(row.rpViews)}</td><td>${nf.format(row.videoPlays)}</td><td>${nf.format(contacts)}</td><td>${nf.format(row.shares)}</td><td>${durationCell}</td></tr>`;
33917    }).join('');
33918  }
33919
33920  let lcStatsRefreshInFlight = false;
33921
33922  async function boot(options = {}){
33923    const silent = !!options.silent;
33924    if (lcStatsRefreshInFlight) return;
33925    lcStatsRefreshInFlight = true;
33926    try {
33927    if (!silent) {
33928      [
33929        'lc-stats-kpis','lc-stats-insights','lc-stats-timeline','lc-stats-funnel',
33930        'lc-stats-category-bars','lc-stats-cabana-list',
33931        'lc-stats-media-list','lc-stats-genetics-list'
33932      ].forEach((id) => {
33933        const node = byId(id);
33934        if (node) node.innerHTML = '<div class="lc-stats-loader"><span></span><span></span><span></span></div>';
33935      });
33936      const top = byId('lc-stats-top-lots');
33937      if (top) top.innerHTML = '<tr><td colspan="8"><div class="lc-stats-loader"><span></span><span></span><span></span></div></td></tr>';
33938    }
33939
33940    let auction = null;
33941    let lots = [];
33942    let summary = { ok: false, lots: [], rps: [], videos: [], images: [], daily: [], hourly: [] };
33943    const state = { ok: false, error: '', updatedAt: new Date() };
33944
33945    try {
33946      auction = await fetchFromBases(CABIN_BASES, `/${AUCTION_ID}`, { noCache: true });
33947      lots = normalize(await fetchFromBases(CABIN_BASES, `/${AUCTION_ID}/lots/`, { noCache: true }));
33948    } catch (error) {
33949      const status = byId('lc-stats-status');
33950      if (status) status.innerHTML = `<strong>No se pudo cargar La Cassina</strong><small>${clean(error.message)}</small>`;
33951      return;
33952    }
33953
33954    try {
33955      summary = normalizeSummary(await fetchFromBases(ANALYTICS_BASES, `/summary?auctionId=${encodeURIComponent(AUCTION_ID)}&limit=250`, { noCache: true }));
33956      state.ok = true;
33957    } catch (error) {
33958      state.error = 'Analytics no disponible todavia';
33959    }
33960
33961    const metaMap = buildMetaMap(lots);
33962    const rows = aggregate(summary, lots, metaMap);
33963    renderHeader(auction, lots, summary, state);
33964    renderKpis(rows, state, summary);
33965    renderInsights(rows, lots, summary);
33966    renderTimeline(summary);
33967    renderFunnelCounts(rows, summary);
33968    const hasSignals = rows.some((row) => row.lotViews || row.rpViews || row.videoPlays || row.imageViews || row.whatsappClicks || row.callClicks || row.contactClicks);
33969    const demandValue = (row) => row.lotViews + row.rpViews + row.videoPlays + row.imageViews;
33970    groupBy(rows, 'category', hasSignals ? demandValue : () => 1, 'lc-stats-category-bars');
33971    groupBy(rows, 'cabana', hasSignals ? demandValue : () => 1, 'lc-stats-cabana-list');
33972    renderMedia(summary, metaMap);
33973    renderCatalogAttributes(lots);
33974    renderTopLots(rows);
33975    } finally {
33976      lcStatsRefreshInFlight = false;
33977    }
33978  }
33979
33980  function scheduleStatsRealtime(){
33981    if (window.__lcStatsRealtimeTimer) clearInterval(window.__lcStatsRealtimeTimer);
33982    window.__lcStatsRealtimeTimer = setInterval(() => {
33983      if (!document.hidden) boot({ silent: true }).catch((error) => console.warn('lacassina stats refresh failed', error));
33984    }, STATS_REFRESH_MS);
33985    document.addEventListener('visibilitychange', () => {
33986      if (!document.hidden) boot({ silent: true }).catch((error) => console.warn('lacassina stats refresh failed', error));
33987    });
33988  }
33989
33990  boot().then(scheduleStatsRealtime).catch((error) => {
33991    console.error('lacassina stats failed', error);
33992    const status = byId('lc-stats-status');
33993    if (status) status.innerHTML = `<strong>Error cargando estadísticas</strong><small>${clean(error.message)}</small>`;
33994  });
33995})();
33996</script>
vendor: 1 bytes, line 33996
33996
33997<script>
33998(function setupPrebidFollower(){
33999  var frame = 0;
34000
34001  function sync(){
34002    frame = 0;
34003    var topOffset = window.innerWidth <= 760 ? 72 : 68;
34004    var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
34005    var hasVisibleLoadFab = Array.prototype.some.call(
34006      document.querySelectorAll('.app-mobile-load-fab'),
34007      function(fab){
34008        var style = window.getComputedStyle(fab);
34009        return fab.getClientRects().length > 0 &&
34010          style.display !== 'none' &&
34011          style.visibility !== 'hidden';
34012      }
34013    );
34014    document.querySelectorAll('.dc-prebid-side-card').forEach(function(card){
34015      var button = card.querySelector('.dc-prebid-side-button');
34016      if (!button) return;
34017      var visibleCard = card.getClientRects().length > 0 && window.getComputedStyle(card).display !== 'none';
34018      var shouldFloat = false;
34019      if (visibleCard) {
34020        var rect = card.getBoundingClientRect();
34021        shouldFloat = rect.bottom <= topOffset;
34022      }
34023      var wasFloating = button.classList.contains('is-scroll-floating');
34024      var wasAboveLoadFab = button.classList.contains('is-above-load-fab');
34025      var positionChanged = wasFloating !== shouldFloat || wasAboveLoadFab !== hasVisibleLoadFab;
34026      var oldRect = positionChanged ? button.getBoundingClientRect() : null;
34027
34028      if (positionChanged && button._prebidPositionAnimation) {
34029        button._prebidPositionAnimation.cancel();
34030        button._prebidPositionAnimation = null;
34031        button.classList.remove('is-position-transitioning');
34032      }
34033      button.classList.remove('is-mobile-floating');
34034      if (positionChanged) button.classList.add('is-position-transitioning');
34035      button.classList.toggle('is-scroll-floating', shouldFloat);
34036      button.classList.toggle('is-above-load-fab', hasVisibleLoadFab);
34037
34038      if (!positionChanged) return;
34039      var newRect = button.getBoundingClientRect();
34040      if (reduceMotion || !button.animate) {
34041        button.classList.remove('is-position-transitioning');
34042        return;
34043      }
34044
34045      var dx = oldRect.left - newRect.left;
34046      var dy = oldRect.top - newRect.top;
34047      var distance = Math.sqrt(dx * dx + dy * dy);
34048      var startScaleX = newRect.width ? oldRect.width / newRect.width : 1;
34049      var startScaleY = newRect.height ? oldRect.height / newRect.height : 1;
34050      var transitionDuration = Math.min(620, Math.max(400, 350 + distance * .28));
34051
34052      var animation = button.animate([
34053        { transform:'translate3d(' + dx + 'px,' + dy + 'px,0) scale(' + startScaleX + ',' + startScaleY + ')', opacity:.92 },
34054        { transform:'translate3d(0,0,0) scale(1,1)', opacity:1 }
34055      ], {
34056        duration:transitionDuration,
34057        easing:'cubic-bezier(.22,1,.36,1)',
34058        fill:'both'
34059      });
34060      button._prebidPositionAnimation = animation;
34061      animation.onfinish = animation.oncancel = function(){
34062        if (button._prebidPositionAnimation !== animation) return;
34063        button._prebidPositionAnimation = null;
34064        button.classList.remove('is-position-transitioning');
34065      };
34066    });
34067  }
34068
34069  function schedule(){
34070    if (frame) return;
34071    frame = window.requestAnimationFrame(sync);
34072  }
34073
34074  window.addEventListener('scroll', schedule, { passive:true });
34075  window.addEventListener('resize', schedule, { passive:true });
34076  window.addEventListener('orientationchange', schedule, { passive:true });
34077  new MutationObserver(schedule).observe(document.body, { childList:true, subtree:true });
34078  schedule();
34079})();
34080</script>
34080
34081</body>
34082</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.