PageSourceSearch

https://simonakam.com/podcast

html simonakam.com collected 2026-10-01 14:53:36 UTC 879,333 bytes, 19,524 lines download raw bytes

1<!DOCTYPE html>
2  <html lang="en">
3  <head>
4  <meta charset="UTF-8">
5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6  <meta name="hydra-site-id" content="btfkih7o">
7  
8  <!-- Primary Meta Tags -->
9  <title>Podcast</title>
10  <meta name="description" content="Podcast — Simon Akam.">
11  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
12  <meta name="author" content="simonakam.com">
13  <meta name="language" content="en">
14  
15  <!-- Canonical URL -->
16  <link rel="canonical" href="https://simonakam.com/podcast">
17  
18  <!-- Favicon -->
19  
20  
21  
22  
23  
24  <!-- Open Graph / Facebook / iMessage / Discord -->
25  <meta property="og:type" content="article">
26  <meta property="og:url" content="https://simonakam.com/podcast">
27  <meta property="og:title" content="Podcast">
28  <meta property="og:description" content="Podcast — Simon Akam.">
29  <meta property="og:site_name" content="simonakam.com">
30  <meta property="og:locale" content="en_US">
31  
32  
33  
34  
35  
36  
37  
38  <!-- Twitter / X Card -->
39  <meta name="twitter:card" content="summary_large_image">
40  <meta name="twitter:site" content="@simonakam.com">
41  <meta name="twitter:url" content="https://simonakam.com/podcast">
42  <meta name="twitter:title" content="Podcast">
43  <meta name="twitter:description" content="Podcast — Simon Akam.">
44  
45  
46  
47  <!-- Additional SEO Meta Tags -->
48  <meta name="theme-color" content="#000000">
49  <meta name="msapplication-TileColor" content="#000000">
50  <meta name="apple-mobile-web-app-capable" content="yes">
51  <meta name="apple-mobile-web-app-status-bar-style" content="default">
52  <meta name="apple-mobile-web-app-title" content="Podcast">
53  
54  <!-- Security Headers -->
55  <meta http-equiv="X-Content-Type-Options" content="nosniff">
56  <meta http-equiv="X-XSS-Protection" content="1; mode=block">
57  <style>
58  /* FOUC gate: opacity on <html> cannot be pierced by child visibility:!important (submenu CSS) */
59  html:not(.hydra-ready) { opacity: 0 !important; }
60  /* Critical: hide nested menus before menu-styles.css arrives */
61  li:not(.expanded) > ol.nested-sortable { display: none !important; }
62  .hydra-seo-nav { position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0; }
63  .hydra-seo-nav ul { list-style:none;margin:0;padding:0; }
64  .hydra-skip-link { position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden; }
65  </style>
66  <noscript><style>html { opacity: 1 !important; }</style></noscript>
67  
67<script type="application/ld+json">
68  {
69  "@context": "https://schema.org",
70  "@type": "WebPage",
71  "name": "Podcast",
72  "description": "Podcast — Simon Akam.",
73  "url": "https://simonakam.com/podcast",
74  "datePublished": "2026-10-01T14:53:34.373Z",
75  "dateModified": "2026-10-01T14:53:34.373Z",
76  "inLanguage": "en-US",
77  "isPartOf": {
78    "@type": "WebSite",
79    "@id": "https://simonakam.com/#website",
80    "name": "simonakam.com",
81    "url": "https://simonakam.com/",
82    "description": "Podcast — Simon Akam.",
83    "publisher": {
84      "@type": "Organization",
85      "name": "simonakam.com",
86      "url": "https://simonakam.com/"
87    }
88  },
89  "breadcrumb": {
90    "@type": "BreadcrumbList",
91    "itemListElement": [
92      {
93        "@type": "ListItem",
94        "position": 1,
95        "name": "Home",
96        "item": "https://simonakam.com/"
97      },
98      {
99        "@type": "ListItem",
100        "position": 2,
101        "name": "Podcast",
102        "item": "https://simonakam.com/podcast"
103      }
104    ]
105  }
106}
107  </script>
107
108  
109  
110  <!-- Preview mode helper script -->
111  
111<script>
112    // Immediately execute to patch request methods before any other script runs
113    (function() {
114      // Detect preview mode by checking hostname
115      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
116      
117      if (isPreviewMode) {
118        console.log("[Preview Helper] Preview mode detected");
119        
120        // Extract GUID from URL path (first segment)
121        const pathParts = window.location.pathname.split('/').filter(Boolean);
122        const previewGuid = pathParts.length > 0 ? pathParts[0] : '';
123        
124        if (!previewGuid) {
125          console.warn("[Preview Helper] No GUID found in path:", window.location.pathname);
126          return; // Exit if no GUID found
127        }
128        
129        console.log("[Preview Helper] Using GUID:", previewGuid);
130        
131        // Store these in window for other scripts
132        window.isPreviewMode = true;
133        window.previewGuid = previewGuid;
134        window.isHydraEnvironment = true;
135        window.Parameters = window.Parameters || {};
136        window.Parameters.isHydra = true;
137        
138        // Helper function that adds the GUID to API URLs and relative links
139        window.getApiUrl = function(endpoint) {
140          // For relative paths
141          if (endpoint.startsWith('/api/')) {
142            const newUrl = '/' + previewGuid + endpoint;
143            console.log("[Preview Helper] Transformed API URL:", endpoint, "→", newUrl);
144            return newUrl;
145          }
146          return endpoint;
147        };
148        
149        // Helper function that adds the GUID to any relative link
150        window.getPreviewUrl = function(url) {
151          // For relative paths (starts with /) that aren't already prefixed with GUID
152          if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
153            const newUrl = '/' + previewGuid + url;
154            console.log("[Preview Helper] Transformed relative link:", url, "→", newUrl);
155            return newUrl;
156          }
157          return url;
158        };
159        
160        // PATCH FETCH API with enhanced URL handling
161        const originalFetch = window.fetch;
162        window.fetch = function(resource, init) {
163          // Only transform string URLs
164          if (typeof resource === 'string') {
165            // Case 1: Handle relative paths starting with /api/
166            if (resource.startsWith('/api/')) {
167              const newUrl = window.getApiUrl(resource);
168              console.log("[Preview Helper] Patched fetch call (API):", resource, "→", newUrl);
169              return originalFetch(newUrl, init);
170            }
171            
172            // Case 2: Handle other relative paths (for content/gallery links)
173            else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) {
174              const newUrl = window.getPreviewUrl(resource);
175              console.log("[Preview Helper] Patched fetch call (relative):", resource, "→", newUrl);
176              return originalFetch(newUrl, init);
177            }
178            
179            // Case 3: Handle absolute URLs to the same domain containing /api/
180            else if (resource.includes(window.location.host) && resource.includes('/api/')) {
181              try {
182                const urlObj = new URL(resource);
183                if (urlObj.hostname === window.location.hostname) {
184                  const path = urlObj.pathname;
185                  // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) — do not double-prefix
186                  if (path.startsWith('/' + previewGuid + '/')) {
187                    return originalFetch(resource, init);
188                  }
189                  const newPath = '/' + previewGuid + path;
190                  urlObj.pathname = newPath;
191                  const newUrl = urlObj.toString();
192                  console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "→", newUrl);
193                  return originalFetch(newUrl, init);
194                }
195              } catch (e) {
196                console.error("[Preview Helper] Error processing URL:", e);
197              }
198            }
199            
200            // Case 4: Handle absolute URLs to the same domain (non-API)
201            else if (resource.includes(window.location.host)) {
202              try {
203                const urlObj = new URL(resource);
204                if (urlObj.hostname === window.location.hostname && 
205                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
206                  const path = urlObj.pathname;
207                  const newPath = '/' + previewGuid + path;
208                  urlObj.pathname = newPath;
209                  const newUrl = urlObj.toString();
210                  console.log("[Preview Helper] Patched fetch call (absolute):", resource, "→", newUrl);
211                  return originalFetch(newUrl, init);
212                }
213              } catch (e) {
214                console.error("[Preview Helper] Error processing URL:", e);
215              }
216            }
217          }
218          return originalFetch(resource, init);
219        };
220        console.log("[Preview Helper] Patched fetch API");
221        
222        // PATCH XMLHttpRequest with enhanced URL handling
223        const originalOpen = XMLHttpRequest.prototype.open;
224        XMLHttpRequest.prototype.open = function(method, url, ...rest) {
225          // Only transform string URLs
226          if (typeof url === 'string') {
227            // Case 1: Handle relative paths starting with /api/
228            if (url.startsWith('/api/')) {
229              const newUrl = window.getApiUrl(url);
230              console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "→", newUrl);
231              return originalOpen.call(this, method, newUrl, ...rest);
232            }
233            
234            // Case 2: Handle other relative paths
235            else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
236              const newUrl = window.getPreviewUrl(url);
237              console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "→", newUrl);
238              return originalOpen.call(this, method, newUrl, ...rest);
239            }
240            
241            // Case 3: Handle absolute URLs to the same domain containing /api/
242            else if (url.includes(window.location.host) && url.includes('/api/')) {
243              try {
244                const urlObj = new URL(url);
245                if (urlObj.hostname === window.location.hostname) {
246                  const path = urlObj.pathname;
247                  if (path.startsWith('/' + previewGuid + '/')) {
248                    return originalOpen.call(this, method, url, ...rest);
249                  }
250                  const newPath = '/' + previewGuid + path;
251                  urlObj.pathname = newPath;
252                  const newUrl = urlObj.toString();
253                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "→", newUrl);
254                  return originalOpen.call(this, method, newUrl, ...rest);
255                }
256              } catch (e) {
257                console.error("[Preview Helper] Error processing URL:", e);
258              }
259            }
260            
261            // Case 4: Handle absolute URLs to the same domain (non-API)
262            else if (url.includes(window.location.host)) {
263              try {
264                const urlObj = new URL(url);
265                if (urlObj.hostname === window.location.hostname && 
266                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
267                  const path = urlObj.pathname;
268                  const newPath = '/' + previewGuid + path;
269                  urlObj.pathname = newPath;
270                  const newUrl = urlObj.toString();
271                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "→", newUrl);
272                  return originalOpen.call(this, method, newUrl, ...rest);
273                }
274              } catch (e) {
275                console.error("[Preview Helper] Error processing URL:", e);
276              }
277            }
278          }
279          return originalOpen.call(this, method, url, ...rest);
280        };
281        console.log("[Preview Helper] Patched XMLHttpRequest");
282        
283        // Note: Link click handling is done by the existing gallery link interceptor
284        // The fetch and XMLHttpRequest patches above handle programmatic requests
285        
286        // Add debugging aid
287        window.debugPreviewUrls = function() {
288          return {
289            hostname: window.location.hostname,
290            pathname: window.location.pathname,
291            guid: window.previewGuid,
292            isPreviewMode: window.isPreviewMode,
293            sampleTransformations: {
294              relativeApi: window.getApiUrl('/api/menu-styles'),
295              relativeLink: window.getPreviewUrl('/gallery'),
296              absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')
297            }
298          };
299        };
300        
301        // Test the transformation immediately
302        console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles'));
303        console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery'));
304        console.log("[Preview Helper] Test absolute URL transformation:", 
305          new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/'));
306      }
307    })();
308  </script>
308
309  
310  
311  
312  
313<!-- Early CDN retry helpers: MUST run before CDN script/link tags.
314     Changing script.src after a load error is unreliable (esp. Firefox defer);
315     always insert a new element pointing at /cdn-proxy/. -->
316<script>
317(function() {
318  window.useCdnProxy = window.useCdnProxy || false;
319  window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || [];
320  function toProxy(url) {
321    if (!url || url.indexOf('cdn.neonsky.app') === -1) return url;
322    return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
323  }
324  function noteRetry(kind, fromUrl, toUrl) {
325    try {
326      window.useCdnProxy = true;
327      var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() };
328      window.__hydraCdnRetryLog.push(entry);
329      console.warn('🔄 CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl);
330    } catch (e) {}
331  }
332  window.__hydraRetryCdnScript = function(el) {
333    try {
334      if (!el || el.getAttribute('data-retried') === 'true') return;
335      var original = el.src || '';
336      if (original.indexOf('cdn.neonsky.app') === -1) return;
337      el.setAttribute('data-retried', 'true');
338      el.onerror = null;
339      var proxyUrl = toProxy(original);
340      noteRetry('script', original, proxyUrl);
341      var s = document.createElement('script');
342      s.src = proxyUrl;
343      s.setAttribute('data-retried', 'true');
344      if (el.id) s.id = el.id;
345      // Dynamic scripts ignore defer; omit it so the proxy copy executes on load.
346      if (el.parentNode) {
347        el.parentNode.insertBefore(s, el.nextSibling);
348      } else {
349        (document.head || document.documentElement).appendChild(s);
350      }
351    } catch (e) {}
352  };
353  window.__hydraRetryCdnLink = function(el) {
354    try {
355      if (!el || el.getAttribute('data-retried') === 'true') return;
356      var original = el.href || '';
357      if (original.indexOf('cdn.neonsky.app') === -1) return;
358      el.setAttribute('data-retried', 'true');
359      el.onerror = null;
360      var proxyUrl = toProxy(original);
361      noteRetry('link', original, proxyUrl);
362      var link = document.createElement('link');
363      link.rel = el.rel || 'stylesheet';
364      if (el.id) link.id = el.id;
365      link.href = proxyUrl;
366      link.setAttribute('data-retried', 'true');
367      if (el.parentNode) {
368        el.parentNode.insertBefore(link, el.nextSibling);
369      } else {
370        (document.head || document.documentElement).appendChild(link);
371      }
372    } catch (e) {}
373  };
374})();
375</script>
375
376<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1790866414373" onerror="window.__hydraRetryCdnLink(this)">
377<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 377
377
378<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 378
378
379<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 379
379
380<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 380
380
381<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
381
382<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1790866414373" onerror="window.__hydraRetryCdnLink(this)">
383<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 383
383
384<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 384
384
385<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 385
385
386<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 386
386
387<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 387
387
388<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 388
388
389<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 389
389
390<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1790866414373" defer onerror="window.__hydraRetryCdnScript(this)"></script>
390
391<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
392<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1790866414373" onerror="window.__hydraRetryCdnLink(this)">
393
394
395
396<!-- CDN Fallback Handler for CSS and JS files -->
397<script>
398(function() {
399  // Global flag: Set to true when any CDN resource fails and uses proxy
400  window.useCdnProxy = window.useCdnProxy || false;
401  
402  // Global base URL function: Returns proxy base if flag is set, otherwise CDN base
403  window.getCdnBaseUrl = function() {
404    return window.useCdnProxy 
405      ? window.location.origin + '/cdn-proxy/'
406      : 'https://cdn.neonsky.app/';
407  };
408  
409  // Utility function to convert CDN URLs to proxy URLs
410  window.convertCdnToProxyUrl = function(cdnUrl) {
411    if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl;
412    return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
413  };
414  
415  // Utility function to construct CDN URLs (uses proxy if flag is set)
416  window.buildCdnUrl = function(path) {
417    return window.getCdnBaseUrl() + path;
418  };
419  
420  // Resource tracking for debugging
421  window._resourceTracking = window._resourceTracking || {
422    resources: [],
423    proxyFlagChanges: [],
424    imageLoads: [],
425    networkErrors: []
426  };
427  
428  // Track when proxy flag changes - use a wrapper function
429  let proxyFlagChangeCount = 0;
430  let _useCdnProxyValue = window.useCdnProxy || false;
431  
432  // Create a setter function that tracks changes
433  window._setUseCdnProxy = function(newValue, reason) {
434    const oldValue = _useCdnProxyValue;
435    if (oldValue !== newValue) {
436      proxyFlagChangeCount++;
437      const changeInfo = {
438        timestamp: new Date().toISOString(),
439        changeNumber: proxyFlagChangeCount,
440        oldValue: oldValue,
441        newValue: newValue,
442        reason: reason || 'Unknown',
443        stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack'
444      };
445      window._resourceTracking.proxyFlagChanges.push(changeInfo);
446      console.error('🔍 PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2));
447      // Keep only last 50 changes
448      if (window._resourceTracking.proxyFlagChanges.length > 50) {
449        window._resourceTracking.proxyFlagChanges.shift();
450      }
451      _useCdnProxyValue = newValue;
452      window.useCdnProxy = newValue;
453    }
454  };
455  
456  // Track resource loading
457  function trackResource(url, type, status, details) {
458    const entry = {
459      timestamp: new Date().toISOString(),
460      url: url,
461      type: type, // 'script', 'link', 'image', etc.
462      status: status, // 'loading', 'success', 'failed', 'proxy', 'storage'
463      details: details || {},
464      useCdnProxy: window.useCdnProxy
465    };
466    window._resourceTracking.resources.push(entry);
467    console.error('📊 RESOURCE_TRACK:', JSON.stringify(entry, null, 2));
468    // Keep only last 200 resources
469    if (window._resourceTracking.resources.length > 200) {
470      window._resourceTracking.resources.shift();
471    }
472  }
473  
474  // Track image loads
475  function trackImageLoad(url, status, source) {
476    const entry = {
477      timestamp: new Date().toISOString(),
478      url: url,
479      status: status, // 'loading', 'success', 'failed'
480      source: source, // 'cdn', 'proxy', 'storage'
481      useCdnProxy: window.useCdnProxy
482    };
483    window._resourceTracking.imageLoads.push(entry);
484    console.error('🖼️ IMAGE_LOAD:', JSON.stringify(entry, null, 2));
485    // Keep only last 100 image loads
486    if (window._resourceTracking.imageLoads.length > 100) {
487      window._resourceTracking.imageLoads.shift();
488    }
489  }
490  
491  // Track network errors
492  function trackNetworkError(url, error, context) {
493    const entry = {
494      timestamp: new Date().toISOString(),
495      url: url,
496      error: {
497        message: error?.message || 'Unknown error',
498        name: error?.name || 'Unknown',
499        type: error?.type || 'unknown',
500        toString: String(error)
501      },
502      context: context || {},
503      useCdnProxy: window.useCdnProxy
504    };
505    window._resourceTracking.networkErrors.push(entry);
506    console.error('❌ NETWORK_ERROR:', JSON.stringify(entry, null, 2));
507    // Keep only last 100 errors
508    if (window._resourceTracking.networkErrors.length > 100) {
509      window._resourceTracking.networkErrors.shift();
510    }
511  }
512  
513  // Expose tracking functions globally
514  window.trackResource = trackResource;
515  window.trackImageLoad = trackImageLoad;
516  window.trackNetworkError = trackNetworkError;
517  
518  // Function to retry failed resources via proxy
519  // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag
520  // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it
521  function retryViaProxy(element, originalSrc, errorEvent) {
522    if (!originalSrc) originalSrc = element.href || element.src;
523    if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return;
524    
525    // Prevent multiple retries
526    if (element.getAttribute('data-retried')) {
527      return;
528    }
529    element.setAttribute('data-retried', 'true');
530    
531    // Check if element still exists in DOM
532    if (!element.parentNode) {
533      console.warn('⚠️ Element no longer in DOM, skipping proxy retry:', originalSrc);
534      return;
535    }
536    
537    // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s
538    // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy
539    // Check error message if available, but default to assuming network error
540    const hasExplicitNetworkError = errorEvent && errorEvent.message && (
541      errorEvent.message.includes('Failed to fetch') ||
542      errorEvent.message.includes('SSL') ||
543      errorEvent.message.includes('secure connection') ||
544      errorEvent.message.includes('network') ||
545      errorEvent.message.includes('ERR_') ||
546      errorEvent.message.includes('SSL_ERROR')
547    );
548    
549    // Track the error
550    trackNetworkError(originalSrc, errorEvent, {
551      elementType: element.tagName,
552      hasExplicitNetworkError: hasExplicitNetworkError,
553      errorMessage: errorEvent?.message || 'No error message',
554      errorName: errorEvent?.name || 'No error name'
555    });
556    
557    // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure)
558    // This covers ISP-affected clients where SSL handshake fails before HTTP request
559    const reason = hasExplicitNetworkError 
560      ? 'Explicit network/SSL error detected'
561      : (!errorEvent || !errorEvent.message)
562        ? 'Browser silent failure (likely SSL/network)'
563        : 'CDN resource failed (using proxy as fallback)';
564    
565    if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) {
566      window._setUseCdnProxy(true, reason);
567      console.error('🚩 CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests');
568      console.error('   Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event');
569    } else {
570      // Even for other errors, set the flag to be safe - proxy can handle 404s too
571      window._setUseCdnProxy(true, reason);
572      console.error('🚩 CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback');
573    }
574    
575    const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
576    console.error('🔄 CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl);
577    
578    // Track the retry
579    trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', {
580      retryUrl: proxyUrl,
581      error: errorEvent?.message || 'Unknown'
582    });
583    
584    if (element.tagName === 'SCRIPT') {
585      const newScript = document.createElement('script');
586      newScript.src = proxyUrl;
587      // Do not copy defer/async onto dynamically inserted scripts. After a CDN
588      // failure, DOMContentLoaded may already have fired; deferred dynamic
589      // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients).
590      newScript.setAttribute('data-retried', 'true');
591      newScript.onload = function() {
592        trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc });
593      };
594      newScript.onerror = function() {
595        trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc });
596        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
597        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
598        trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
599        newScript.src = storageUrl;
600        newScript.onload = function() {
601          trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc });
602        };
603      };
604      element.parentNode.replaceChild(newScript, element);
605    } else if (element.tagName === 'LINK') {
606      const newLink = document.createElement('link');
607      newLink.rel = element.rel;
608      newLink.href = proxyUrl;
609      newLink.setAttribute('data-retried', 'true');
610      newLink.onload = function() {
611        trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc });
612      };
613      newLink.onerror = function() {
614        trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc });
615        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
616        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
617        trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
618        newLink.href = storageUrl;
619        newLink.onload = function() {
620          trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc });
621        };
622      };
623      element.parentNode.replaceChild(newLink, element);
624    }
625  }
626  
627  // Monitor script and link tags for errors
628  document.addEventListener('error', function(e) {
629    const target = e.target;
630    if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 
631        target.src && target.src.includes('cdn.neonsky.app')) {
632      retryViaProxy(target, target.src, e);
633    } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) {
634      retryViaProxy(target, target.href, e);
635    }
636  }, true);
637  
638  // Also check existing scripts/links that may have failed before this script loaded
639  // For ISP-affected clients, we assume CDN failures are network/SSL errors
640  setTimeout(function() {
641    document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) {
642      if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
643        el.addEventListener('error', function(errorEvent) {
644          // For ISP-affected clients, assume any CDN failure is a network/SSL error
645          // The proxy can handle both network errors and 404s
646          retryViaProxy(el, el.src, errorEvent);
647        });
648      } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
649        // CSS link failed if it has no stylesheet - check after a delay
650        setTimeout(function() {
651          if (!el.sheet && !el.getAttribute('data-retried')) {
652            // For ISP-affected clients, assume any CDN failure is a network/SSL error
653            retryViaProxy(el, el.href, null);
654          }
655        }, 2000);
656      }
657    });
658  }, 100);
659})();
660</script>
660
661
662<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
663<script>
664(function() {
665  'use strict';
666  
667  // Only initialize if not already initialized
668  if (window._indexLogCaptureInitialized) {
669    return;
670  }
671  window._indexLogCaptureInitialized = true;
672  
673  // Wait for DOM to be ready
674  function initLogCapture() {
675    // Store original console methods
676    const originalConsole = {
677      log: console.log,
678      warn: console.warn,
679      error: console.error,
680      info: console.info
681    };
682
683    // Array to store all console messages
684    window._capturedLogs = window._capturedLogs || [];
685
686    // Override console methods to capture logs
687    ['log', 'warn', 'error', 'info'].forEach(method => {
688      console[method] = function(...args) {
689        // Call original method
690        originalConsole[method].apply(console, args);
691        
692        // Capture the log with timestamp
693        window._capturedLogs.push({
694          timestamp: new Date().toISOString(),
695          level: method.toUpperCase(),
696          message: args.map(function(arg) {
697            if (arg !== null && arg !== undefined && Object(arg) === arg) {
698              try {
699                return JSON.stringify(arg, null, 2);
700              } catch (e) {
701                return String(arg);
702              }
703            }
704            return String(arg);
705          }).join(' ')
706        });
707        
708        // Keep only last 2000 logs to prevent memory issues
709        if (window._capturedLogs.length > 2000) {
710          window._capturedLogs.shift();
711        }
712      };
713    });
714
715    // Add keyboard shortcut to download logs (S+J)
716    let pressedKeys = new Set();
717    
718    document.addEventListener('keydown', function(event) {
719      const key = (event.key || '').toLowerCase();
720      
721      if (key === 's' || key === 'j') {
722        pressedKeys.add(key);
723        
724        // Check if both S and J are pressed
725        if (pressedKeys.has('s') && pressedKeys.has('j')) {
726          event.preventDefault();
727          console.log('S+J detected - downloading logs...');
728          
729          // Show a brief visual indicator
730          const indicator = document.createElement('div');
731          indicator.style.cssText = 
732            'position: fixed; top: 20px; right: 20px; z-index: 10000; ' +
733            'background: #4CAF50; color: white; padding: 10px 20px; ' +
734            'border-radius: 5px; font-family: Arial, sans-serif; ' +
735            'font-size: 14px; font-weight: bold; ' +
736            'box-shadow: 0 2px 10px rgba(0,0,0,0.3);';
737          indicator.textContent = '📥 Downloading logs...';
738          document.body.appendChild(indicator);
739          
740          setTimeout(() => {
741            if (indicator.parentNode) {
742              indicator.parentNode.removeChild(indicator);
743            }
744          }, 2000);
745          
746          downloadLogs();
747          // Clear the set after triggering
748          pressedKeys.clear();
749        }
750      }
751    });
752    
753    document.addEventListener('keyup', function(event) {
754      const key = (event.key || '').toLowerCase();
755      if (key === 's' || key === 'j') {
756        pressedKeys.delete(key);
757      }
758    });
759
760    console.log('📋 Log capture system initialized. Press S+J simultaneously to download logs.');
761  }
762
763  // Download captured logs as a text file
764  function downloadLogs() {
765    try {
766      // Get all scripts and links, categorized
767      const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
768      const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href);
769      
770      // Get additional debug info
771      const debugInfo = {
772        url: window.location.href,
773        userAgent: navigator.userAgent,
774        timestamp: new Date().toISOString(),
775        useCdnProxy: window.useCdnProxy || false,
776        windowLocation: window.location.toString(),
777        documentReadyState: document.readyState,
778        // All scripts/links (not just CDN)
779        allScriptsLoaded: allScripts,
780        allLinksLoaded: allLinks,
781        // CDN-specific (for backward compatibility and CDN debugging)
782        scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')),
783        linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')),
784        // Gallery scripts (loaded from Worker)
785        galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')),
786        galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')),
787        // Failed resources
788        failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src),
789        failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href),
790        // Proxy resources
791        proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')),
792        proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')),
793        // Resource tracking data
794        resourceTracking: window._resourceTracking || {
795          resources: [],
796          proxyFlagChanges: [],
797          imageLoads: [],
798          networkErrors: []
799        },
800        // Performance timing
801        performanceTiming: performance.timing ? {
802          navigationStart: performance.timing.navigationStart,
803          domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
804          loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
805          domInteractive: performance.timing.domInteractive - performance.timing.navigationStart
806        } : null,
807        // Style / logo diagnostics (layout/color bug triage)
808        styleDiagnostics: (function() {
809          try {
810            var rootStyle = getComputedStyle(document.documentElement);
811            var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img'));
812            return {
813              siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(),
814              menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(),
815              menuColor: rootStyle.getPropertyValue('--menu-color').trim(),
816              inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'),
817              inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'),
818              hasMenuStyleCustomizer: !!window.MenuStyleCustomizer,
819              menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized),
820              menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
821                ? {
822                    siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor,
823                    menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor,
824                    color: window.MenuStyleCustomizer.settings.color
825                  }
826                : null,
827              hasSidebarManager: !!window.SidebarManager,
828              sidebarElementCount: window.SidebarManager && window.SidebarManager.elements
829                ? window.SidebarManager.elements.length
830                : 0,
831              hasPageManager: !!window.PageManager,
832              pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'),
833              pageElementCount: (function() {
834                try {
835                  return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length;
836                } catch (e) { return -1; }
837              })(),
838              pageContainerHtmlLength: (function() {
839                try {
840                  var pc = document.querySelector('.gallery-container .page-container, .page-container');
841                  return pc ? (pc.innerHTML || '').length : 0;
842                } catch (e) { return -1; }
843              })(),
844              activeContentType: window._activeContentType || null,
845              logoImageCount: logoNodes.length,
846              logoImages: logoNodes.slice(0, 5).map(function(img) {
847                return {
848                  src: img.currentSrc || img.src || '',
849                  naturalWidth: img.naturalWidth || 0,
850                  display: getComputedStyle(img).display,
851                  complete: !!img.complete
852                };
853              }),
854              earlyCdnRetries: window.__hydraCdnRetryLog || []
855            };
856          } catch (e) {
857            return { error: String(e && e.message ? e.message : e) };
858          }
859        })()
860      };
861
862      // Create log content
863      const logContent = [
864        '=== INDEX.TS DEBUG LOGS ===',
865        'Generated: ' + debugInfo.timestamp,
866        'URL: ' + debugInfo.url,
867        'User Agent: ' + debugInfo.userAgent,
868        'useCdnProxy Flag: ' + debugInfo.useCdnProxy,
869        'Document Ready State: ' + debugInfo.documentReadyState,
870        '================================',
871        'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.',
872        'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).',
873        '================================',
874        '',
875        '=== DEBUG INFO ===',
876        JSON.stringify(debugInfo, null, 2),
877        '',
878        '=== STYLE DIAGNOSTICS ===',
879        JSON.stringify(debugInfo.styleDiagnostics, null, 2),
880        '',
881        '=== RESOURCE TRACKING SUMMARY ===',
882        'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0),
883        'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0),
884        'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0),
885        'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0),
886        '',
887        '=== PROXY FLAG CHANGES ===',
888        JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2),
889        '',
890        '=== RESOURCE LOADING TRACKING ===',
891        JSON.stringify(debugInfo.resourceTracking.resources, null, 2),
892        '',
893        '=== IMAGE LOADING TRACKING ===',
894        JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2),
895        '',
896        '=== NETWORK ERRORS ===',
897        JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2),
898        '',
899        '=== CONSOLE LOGS ===',
900        ...(window._capturedLogs || []).map(log => 
901          '[' + log.timestamp + '] ' + log.level + ': ' + log.message
902        )
903      ].join('\n');
904
905      // Create and download file
906      const blob = new Blob([logContent], { type: 'text/plain' });
907      const url = URL.createObjectURL(blob);
908      const a = document.createElement('a');
909      a.href = url;
910      a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
911      document.body.appendChild(a);
912      a.click();
913      document.body.removeChild(a);
914      URL.revokeObjectURL(url);
915
916      console.log('✅ Debug logs downloaded successfully!');
917    } catch (error) {
918      console.error('❌ Failed to download logs:', error);
919      alert('Failed to download logs. Error: ' + (error.message || 'Unknown error'));
920    }
921  }
922
923  // Initialize when DOM is ready
924  if (document.readyState === 'loading') {
925    document.addEventListener('DOMContentLoaded', initLogCapture);
926  } else {
927    // DOM already ready, initialize immediately
928    setTimeout(initLogCapture, 100);
929  }
930})();
931</script>
931
932
933<!-- Add auth token if available -->
934
935
936<style>
937/* Override Pico.css body padding to prevent jumping between pages */
938body {
939  padding: 0 !important;
940  margin: 0 !important;
941  display: block !important;
942}
943
944/* Scope Pico.css to only affect elements in edit mode */
945:root:not([data-theme="pico"]) {
946--pico-applied: dark;
947}
948
949/* Add data-theme="pico" to body when in edit mode */
950body.edit-mode-active {
951data-theme: dark;
952}
953
954/* Custom styles to ensure Pico only affects editor UI, not content */
955.sidebar.editing button,
956.edit-controls button,
957.element-header button,
958.text-editor button,
959.image-uploader button {
960--pico-applied: dark;
961}
962
963/* Hide default browser-styling when Pico is active */
964[data-theme="pico"] button.btn {
965appearance: dark;
966-webkit-appearance: dark;
967}
968
969/* Ensure content isn't affected by Pico styles */
970.gallery-container,
971.page-text,
972.sidebar-text {
973--pico-applied: none !important;
974}
975
976/* Conflict Indicator Styles */
977#conflictIndicator {
978  background: #d32f2f !important;
979  border: 1px solid #d32f2f !important;
980  border-radius: 4px;
981  padding: 8px;
982  cursor: pointer;
983  transition: opacity 0.2s;
984  margin-left: 8px;
985  display: flex; /* Use flex for centering, but hidden by default via inline style */
986  align-items: center;
987  justify-content: center;
988}
989
990#conflictIndicator:hover {
991  background: #e53935 !important;
992  border-color: #e53935 !important;
993  opacity: 0.9;
994}
995
996#conflictIndicator svg {
997  display: block;
998  width: 24px;
999  height: 24px;
1000  margin: 0 auto;
1001  color: white !important;
1002}
1003
1004/* Tooltip styles for conflict indicator - allow wrapping */
1005#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"],
1006#tooltip-container .tooltip {
1007  max-width: 200px;
1008  word-wrap: break-word;
1009  white-space: normal;
1010  line-height: 1.4;
1011}
1012
1013.global-header-controls {
1014  display: flex;
1015  align-items: center;
1016  gap: 8px;
1017}
1018
1019.sidebar-header {
1020  display: flex;
1021  align-items: center;
1022  gap: 8px;
1023}
1024
1025/* Cookie Notice — paper UI (public-safe; does not depend on edit-mode pe-* scopes) */
1026.cookie-notice {
1027  --cn-paper: #f6f5f1;
1028  --cn-paper-elevated: #fdfcfa;
1029  --cn-ink: #2c2a27;
1030  --cn-ink-muted: #6b6760;
1031  --cn-ink-faint: #9b968d;
1032  --cn-rule: rgba(44, 42, 39, 0.1);
1033  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1034  --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1);
1035  --cn-radius: 3px;
1036  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1037
1038  position: fixed;
1039  bottom: 0;
1040  left: 0;
1041  right: 0;
1042  z-index: 10000;
1043  display: flex;
1044  align-items: center;
1045  justify-content: space-between;
1046  flex-wrap: wrap;
1047  gap: 0.85rem 1.25rem;
1048  padding: 1.1rem 1.35rem 1.2rem;
1049  font-family: var(--cn-font);
1050  font-size: 14px;
1051  line-height: 1.45;
1052  color: var(--cn-ink);
1053  background: var(--cn-paper-elevated);
1054  border-top: 1px solid var(--cn-rule);
1055  box-shadow: var(--cn-shadow);
1056}
1057
1058.cookie-notice.hidden {
1059  display: none;
1060}
1061
1062.cookie-notice-text {
1063  flex: 1 1 220px;
1064  min-width: 0;
1065  margin: 0;
1066  color: var(--cn-ink-muted);
1067  font-size: 13px;
1068}
1069
1070.cookie-notice-text a.cookie-notice-policy {
1071  color: var(--cn-ink);
1072  font-weight: 600;
1073  text-decoration: underline;
1074  text-underline-offset: 2px;
1075}
1076
1077.cookie-notice-text a.cookie-notice-policy:hover {
1078  color: var(--cn-ink-muted);
1079}
1080
1081.cookie-notice-buttons {
1082  display: flex;
1083  flex-wrap: wrap;
1084  align-items: center;
1085  justify-content: flex-end;
1086  gap: 0.5rem;
1087}
1088
1089.cookie-notice .pe-btn {
1090  display: inline-flex;
1091  align-items: center;
1092  justify-content: center;
1093  gap: 0.4rem;
1094  margin: 0;
1095  padding: 0.45rem 0.85rem;
1096  font-family: var(--cn-font);
1097  font-size: 11px;
1098  font-weight: 600;
1099  letter-spacing: 0.1em;
1100  text-transform: uppercase;
1101  color: var(--cn-ink-muted);
1102  background: var(--cn-paper);
1103  border: 1px solid var(--cn-rule-strong);
1104  border-radius: var(--cn-radius);
1105  box-shadow: none;
1106  cursor: pointer;
1107  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1108}
1109
1110.cookie-notice .pe-btn:hover {
1111  color: var(--cn-ink);
1112  background: var(--cn-paper-elevated);
1113  border-color: rgba(44, 42, 39, 0.22);
1114}
1115
1116.cookie-notice .pe-btn--primary {
1117  color: var(--cn-ink);
1118  background: var(--cn-paper);
1119  border-color: var(--cn-rule-strong);
1120}
1121
1122.cookie-notice .pe-btn--primary:hover {
1123  color: var(--cn-ink);
1124  background: var(--cn-paper-elevated);
1125  border-color: rgba(44, 42, 39, 0.22);
1126}
1127
1128#hydraDoNotSellLink {
1129  --cn-paper: #f6f5f1;
1130  --cn-paper-elevated: #fdfcfa;
1131  --cn-ink: #2c2a27;
1132  --cn-ink-muted: #6b6760;
1133  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1134  --cn-radius: 3px;
1135  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1136
1137  position: fixed;
1138  bottom: 10px;
1139  left: 12px;
1140  z-index: 9999;
1141  display: inline-flex;
1142  align-items: center;
1143  justify-content: center;
1144  margin: 0;
1145  padding: 0.45rem 0.85rem;
1146  font-family: var(--cn-font);
1147  font-size: 11px;
1148  font-weight: 600;
1149  letter-spacing: 0.1em;
1150  text-transform: uppercase;
1151  color: var(--cn-ink-muted);
1152  background: var(--cn-paper-elevated);
1153  border: 1px solid var(--cn-rule-strong);
1154  border-radius: var(--cn-radius);
1155  box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06);
1156  cursor: pointer;
1157}
1158
1159#hydraDoNotSellLink.hidden {
1160  display: none;
1161}
1162
1163#hydraDoNotSellLink:hover {
1164  color: var(--cn-ink);
1165  background: var(--cn-paper);
1166  border-color: rgba(44, 42, 39, 0.22);
1167}
1168
1169/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */
1170html.hydra-ready {
1171  opacity: 1;
1172  transition: opacity 0.2s ease;
1173}
1174/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */
1175body.hydra-ready {
1176  visibility: visible;
1177}
1178
1179/* Admin panel typography */
1180.sidebar-element-form,
1181.sidebar-element-form *,
1182.add-form,
1183.add-form *,
1184.metadata-editor,
1185.metadata-editor * {
1186  font-family: "Hanken Grotesk", sans-serif;
1187  font-weight: 200;
1188  letter-spacing: 0.02em;
1189}
1190
1191.sidebar-element-form h3,
1192.sidebar-element-form label,
1193.add-form label,
1194.metadata-editor h3,
1195.metadata-editor label {
1196  text-transform: uppercase;
1197  letter-spacing: 0.04em;
1198  font-weight: 300;
1199}
1200
1201.sidebar-element-form .form-actions .btn,
1202.sidebar-element-form button,
1203.add-form .form-actions .btn,
1204.add-form button,
1205.metadata-editor .form-actions .btn,
1206.metadata-editor button {
1207  text-transform: uppercase;
1208  letter-spacing: 0.08em;
1209  font-weight: 300;
1210}
1211
1212.sidebar-element-form input,
1213.sidebar-element-form select,
1214.sidebar-element-form textarea,
1215.sidebar-element-form button,
1216.add-form input,
1217.add-form select,
1218.add-form textarea,
1219.add-form button,
1220.metadata-editor input,
1221.metadata-editor select,
1222.metadata-editor textarea,
1223.metadata-editor button {
1224  font-family: "Hanken Grotesk", sans-serif;
1225  font-weight: 200;
1226  letter-spacing: 0.02em;
1227  border-radius: 0;
1228}
1229</style>
1230
1231<script>
1232// Flag to prevent duplicate initialization
1233window.hydraInitialized = false;
1234
1235function ensureAdminPanelFont() {
1236  const head = document.head || document.getElementsByTagName('head')[0];
1237  if (!head) {
1238    return;
1239  }
1240
1241  const linkDefinitions = [
1242    {
1243      id: 'admin-panels-font-preconnect-googleapis',
1244      rel: 'preconnect',
1245      href: 'https://fonts.googleapis.com'
1246    },
1247    {
1248      id: 'admin-panels-font-preconnect-gstatic',
1249      rel: 'preconnect',
1250      href: 'https://fonts.gstatic.com',
1251      crossOrigin: 'anonymous'
1252    },
1253    {
1254      id: 'admin-panels-font-stylesheet',
1255      rel: 'stylesheet',
1256      href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap'
1257    },
1258    {
1259      id: 'admin-panels-font-barlow',
1260      rel: 'stylesheet',
1261      href: 'https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,200;0,400;1,200;1,400&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap'
1262    }
1263  ];
1264
1265  for (const definition of linkDefinitions) {
1266    if (!document.getElementById(definition.id)) {
1267      const link = document.createElement('link');
1268      link.id = definition.id;
1269      link.rel = definition.rel;
1270      link.href = definition.href;
1271
1272      if (definition.crossOrigin) {
1273        link.crossOrigin = definition.crossOrigin;
1274      }
1275
1276      head.appendChild(link);
1277    }
1278  }
1279}
1280
1281ensureAdminPanelFont();
1282
1283 window.Parameters = window.Parameters || {};
1284  window.Parameters.siteId = "btfkih7o";
1285  window.Parameters.SiteId = "btfkih7o"; // Also set capitalized version
1286  window.siteId = "btfkih7o";
1287  window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp";
1288  window.NEON_SKY_FILES = window.NEON_SKY_FILES || {};
1289  window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js';
1290  window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css';
1291  window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json';
1292
1293// Initialize menu styles from server
1294document.addEventListener('DOMContentLoaded', function() {
1295// Apply initial menu styles from server
1296const initialMenuStyles = {"fontFamily":"Helvetica, sans-serif","fontSize":"12px","color":"#757575","activeColor":"#404040","hoverColor":"#000000","verticalSpacing":"2px","menuBackgroundColor":"#ffffff","siteBackgroundColor":"#ffffff","mobileHeaderHeight":"60px","sidebarWidth":"15vw","sidebarPaddingTop":"0px","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","menuLayout":"sidebar","fontName":"Helvetica"};
1297window.__hydraStyleFromQuery = null;
1298const initialSiteThemeRuntimeVars = null;
1299
1300// Set CSS custom properties
1301// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override).
1302if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) {
1303  Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) {
1304    document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]);
1305  });
1306  if (initialMenuStyles && initialMenuStyles.verticalSpacing) {
1307    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1308  }
1309  // Match menu chrome / Gallery Options overview preview (light weight)
1310  document.documentElement.style.setProperty('--menu-font-weight', '200');
1311  if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1312    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1313  }
1314} else if (initialMenuStyles) {
1315  if (initialMenuStyles.fontFamily) {
1316    document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily);
1317  }
1318  if (initialMenuStyles.fontSize) {
1319    document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize);
1320  }
1321  // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size
1322  if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1323    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1324  }
1325  document.documentElement.style.setProperty('--menu-font-weight', '200');
1326  if (initialMenuStyles.color) {
1327    document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color);
1328  }
1329  if (initialMenuStyles.activeColor) {
1330    document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor);
1331  }
1332  if (initialMenuStyles.hoverColor) {
1333    document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor);
1334  }
1335  // Background colors must be applied here too: MenuStyleCustomizer may load late/fail
1336  // after CDN→proxy retry, and CSS defaults are white/#fafafa.
1337  if (initialMenuStyles.siteBackgroundColor) {
1338    document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor);
1339  }
1340  if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) {
1341    document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor);
1342  }
1343  if (initialMenuStyles.verticalSpacing) {
1344    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1345  }
1346  if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') {
1347    var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim();
1348    document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')');
1349  }
1350  var resolvedSiteBg = '';
1351  if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') {
1352    resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true });
1353  } else if (initialMenuStyles.siteBackgroundImageUrl) {
1354    resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim();
1355  }
1356  if (resolvedSiteBg) {
1357    var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1;
1358    var siteBgImgCss = peIsCssGradient
1359      ? resolvedSiteBg
1360      : 'url(' + JSON.stringify(resolvedSiteBg) + ')';
1361    document.documentElement.style.setProperty('--site-background-image', siteBgImgCss);
1362    var siteBgSize = 'cover';
1363    if (peIsCssGradient) {
1364      siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroundSize
1365        ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg)
1366        : '100% 100%';
1367      document.body.classList.add('has-site-background-gradient');
1368    }
1369    document.documentElement.style.setProperty('--site-background-size', siteBgSize);
1370  }
1371  if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') {
1372    var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim();
1373    /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */
1374    var mhhParsed = parseInt(mhh0, 10);
1375    if (Number.isNaN(mhhParsed) || mhhParsed < 1) {
1376      mhh0 = '60px';
1377    }
1378    document.documentElement.style.setProperty('--mobile-header-height', mhh0);
1379    document.documentElement.style.setProperty('--content-margin-top', mhh0);
1380    document.documentElement.style.setProperty('--top-bar-height-setting', mhh0);
1381  }
1382}
1383
1384if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) {
1385  document.body.classList.add('menu-background-hidden');
1386  document.documentElement.style.setProperty('--menu-background-color', 'transparent');
1387  document.documentElement.style.setProperty('--menu-background-image', 'none');
1388}
1389
1390if (initialMenuStyles && initialMenuStyles.menuTextUppercase) {
1391  document.body.classList.add('menu-text-uppercase');
1392  if (!document.getElementById('menu-text-uppercase-style')) {
1393    var menuUpperStyle = document.createElement('style');
1394    menuUpperStyle.id = 'menu-text-uppercase-style';
1395    menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}';
1396    document.head.appendChild(menuUpperStyle);
1397  }
1398}
1399
1400// Store initial styles in MenuStyleCustomizer if it exists
1401if (window.MenuStyleCustomizer) {
1402  window.MenuStyleCustomizer.settings = {
1403    ...window.MenuStyleCustomizer.defaults,
1404    ...initialMenuStyles
1405  };
1406}
1407});
1408</script>
vendor: 1 bytes, line 1408
1408
1409<script>
1410let magic = null;
1411let userMetadata = null;
1412let isAuthenticated = false;
1413let isAdmin = false;
1414let didToken = null;
1415let siteId = "btfkih7o";
1416
1417/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */
1418function clearHydraClientAuthStorage() {
1419  try {
1420    localStorage.removeItem('hydra_is_admin');
1421    localStorage.removeItem('hydra_auth_token');
1422    localStorage.removeItem('hydra_auth_email');
1423    localStorage.removeItem('hydra_did_token');
1424    localStorage.removeItem('hydra_auth_site');
1425    localStorage.removeItem('hydra_auth_last_refresh');
1426    localStorage.removeItem('hydra_auth_auto_refresh');
1427  } catch (e) {}
1428  if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') {
1429    try { window.TokenManager.clearToken(); } catch (e) {}
1430  }
1431  isAuthenticated = false;
1432  isAdmin = false;
1433  didToken = null;
1434  userMetadata = null;
1435  window.isAuthenticated = false;
1436  window.isAdmin = false;
1437  window.didToken = null;
1438}
1439
1440function hydraUrlRequestsLogout() {
1441  try {
1442    var params = new URLSearchParams(window.location.search);
1443    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1444    if (params.get('logout') === '1' || params.get('logout') === 'true') return true;
1445    if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true;
1446    return path === '/_manage/logout';
1447  } catch (e) {
1448    return false;
1449  }
1450}
1451
1452function hydraCleanLogoutFromUrl() {
1453  try {
1454    var params = new URLSearchParams(window.location.search);
1455    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1456    var hadQuery = params.has('logout') || params.has('hydra_logout');
1457    params.delete('logout');
1458    params.delete('hydra_logout');
1459    var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname;
1460    var qs = params.toString();
1461    var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || '');
1462    if (hadQuery || path === '/_manage/logout') {
1463      history.replaceState({}, '', next);
1464    }
1465  } catch (e) {}
1466}
1467
1468(function hydraLogoutFromUrlEarly() {
1469  if (!hydraUrlRequestsLogout()) return;
1470  clearHydraClientAuthStorage();
1471  try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {}
1472  hydraCleanLogoutFromUrl();
1473  console.log('[Hydra] Auth cleared via logout link/query.');
1474})();
1475
1476function hydraUrlRequestsRefresh() {
1477  try {
1478    var params = new URLSearchParams(window.location.search);
1479    var v = params.get('refresh');
1480    return v === '1' || v === 'true';
1481  } catch (e) {
1482    return false;
1483  }
1484}
1485
1486function hydraClearGalleryClientCache() {
1487  try {
1488    Object.keys(localStorage).filter(function (key) {
1489      return key.includes('gallery') || key.includes('neon') || key.includes('lightbox');
1490    }
1490).forEach(function (key) {
1491      localStorage.removeItem(key);
1492    });
1493  } catch (e) {}
1494  try {
1495    delete window.__neonGalleryMainBootstrapWipeDone;
1496  } catch (e) {
1497    window.__neonGalleryMainBootstrapWipeDone = false;
1498  }
1499  window._forceGalleryReload = true;
1500}
1501
1502function hydraCleanRefreshFromUrl() {
1503  try {
1504    var params = new URLSearchParams(window.location.search);
1505    if (!params.has('refresh')) return;
1506    params.delete('refresh');
1507    var qs = params.toString();
1508    var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || '');
1509    history.replaceState({}, '', next);
1510  } catch (e) {}
1511}
1512
1513(function hydraRefreshFromUrlEarly() {
1514  if (!hydraUrlRequestsRefresh()) return;
1515  clearHydraClientAuthStorage();
1516  hydraClearGalleryClientCache();
1517  try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {}
1518  hydraCleanRefreshFromUrl();
1519  console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 — forcing fresh load.');
1520})();
1521
1522// State Management — site-wide KV metadata for the editor (not per-page SEO)
1523window.siteMetadata = {"title":"Simon Akam","description":"Email [email protected] · Representation Patrick Walsh","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":""};
1524console.log('Client Initialized site metadata:', window.siteMetadata);
1525
1526/** Match SSR title format: "{Gallery Title} | {Site Title}" */
1527function formatGalleryDocumentTitle(galleryTitle, win) {
1528  win = win || window;
1529  var meta = win.siteMetadata;
1530  var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname;
1531  return (galleryTitle || 'Gallery') + ' | ' + siteTitle;
1532}
1533
1534console.log('Client Google Analytics from metadataToUse:', '');
1535console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics);
1536console.log('Client Content Language:', window.siteMetadata.contentLanguage);
1537
1538// State Management
1539let galleries = [{"id":1754872130103,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true},{"id":1754872727324,"title":"Simon, Start here...","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"qnx9or4j","visible":false,"parentId":null,"siteId":"btfkih7o","slug":"start-here","url":"/start-here","pageElements":[{"id":1754872731222,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"About","metaDescription":"","metaKeywords":""},{"id":1754872734681,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754872730270,"type":"column-container","title":"Two Column Layout","visible":true,"position":2,"columns":[{"id":1754870444536,"hAlign":"left","vAlign":"middle","elements":[{"id":1754870444336,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">Simon,</p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">To log into the new Site Manager...</p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">1) Press the <span style=\"color: rgb(255, 0, 200);\"> </span><span style=\"font-size: 24px; color: rgb(255, 0, 200);\">+</span><span style=\"font-size: 24px;\"> </span> key on your keyboard</p><p style=\"margin-bottom: 0.5em;\">2) Enter your email address.</p><p style=\"margin-bottom: 0.5em;\">3) Enter the passcode that you receive via your email...</p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">...then click the Edit button in the top-left corner.</p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">There's a video how-to below. </p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">The new manager suppors text editing and formatting much better than the old one. Let me know if you need help.</p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\">Jayson</p><p style=\"margin-bottom: 0.5em;\">Neon Sky</p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><br></p>","textWidth":61}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}},{"id":1754872893447,"type":"embedded-content","title":"New Element","visible":true,"position":3,"contentUrl":"https://share.descript.com/embed/FyubGmgsVIG","containerWidth":81,"containerHeight":71,"useAutoHeight":false}
1539,{"id":1754872925074,"type":"text","title":"Text Block","visible":true,"position":4,"textContent":"<p> </p><p><br></p>","textWidth":100}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false,"position":9,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1754870441126,"title":"Home","url":"/home","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":false,"parentId":null,"siteId":"btfkih7o","pageId":"jduz6xoh","classicGuid":"4bd5ec0737e49","importSource":"classic-gallery","slug":"home","normalizedName":"home","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0737e49","importedFromClassic":true,"siteId":"btfkih7o","pageId":"jduz6xoh","horizontalScrollerText":"<p><br></p>","siteAlias":"preview.neonsky.app","initialPageUuid":"jduz6xoh","initialPageAlias":"home","timestamp":1754871740613,"startInSingles":true,"layoutType":"singles","columns":"3","spacing":2,"autoplaySingles":true,"autoplayDuration":"3","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=4bd5ec0737e49":{"isClassicCollection":true,"guid":"GUID=4bd5ec0737e49","metadata":{},"totalItems":0}}},"siteAlias":null,"isHomePage":true,"metaTitle":"Simon Akam","metaDescription":"Email [email protected] · Representation Patrick Walsh","hideMenuOnPage":false,"passwordProtected":false},{"id":1754870441128,"title":"The Changing Of The Guard","isFolder":true,"isCollapsed":false,"visible":true,"parentId":null,"siteId":"btfkih7o","slug":"the-changing-of-the-guard","importSource":"folder","isSubmenu":true,"isHomePage":false},{"id":1754870441129,"title":"About the book","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1754870441129","visible":true,"parentId":1754870441128,"siteId":"btfkih7o","slug":"about-the-book","url":"/about-the-book","pageElements":[{"id":1754870442129,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"About the book","metaDescription":"","metaKeywords":""},{"id":1754872052274,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754870444129,"type":"column-container","title":"Two Column Layout","visible":true,"position":2,"columns":[{"id":1754870444429,"hAlign":"center","vAlign":"top","elements":[{"id":1754870444229,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754870931144_TheChangingOfTheGuard_B_concept_LT02.jpg","imageKey":"btfkih7o/1754870931144_TheChangingOfTheGuard_B_concept_LT02.jpg","imageFilename":"TheChangingOfTheGuard_B_concept_LT02.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":94,"slideshowHeight":71,"showFullImages":true}]},{"id":1754870444529,"hAlign":"left","vAlign":"middle","elements":[{"id":1754870444329,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>A revelatory, explosive analysis of the military today</b></span></p><p>\n</p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Over the first two decades of the twenty-first century, Britain has changed enormously. During this time, the British Army fought two campaigns, in Iraq and Afghanistan, at considerable financial and human cost. Yet neither war achieved its objectives. This book questions why, and provides challenging but necessary answers.</span></p><p>\n</p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Composed from assiduous documentary research, field reportage, and hundreds of interviews with many soldiers and officers who served, as well as the politicians who directed them, the allies who accompanied them, and the family members who loved and — on occasion — lost them, it is a strikingly rich, nuanced portrait of one of our pivotal national institutions in a time of great stress.</span></p><p>\n</p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Award-winning journalist Simon Akam, who spent a year in the army when he was 18, returned a decade later to see how the institution had changed. His book examines the relevance of the armed forces today — their social, economic, political, and cultural role. This is as much a book about Britain, and about the politics of failure, as it is about the military.</span></p><p>\n</p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The Changing of the Guard was published by Scribe in February 2021. Read more <a href=\"https://scribepublications.co.uk/books-authors/books/the-changing-of-the-guard\" target=\"_blank\">here</a>.</span></p>","textWidth":92}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}
1539,{"id":1754871039169,"type":"text","title":"Text Block","visible":true,"position":3,"textContent":"<p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Endorsements to the book  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Winner of the Templer First Book Award 2021 </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A “state-of-the-nation” book of resounding power, deep conviction and far-reaching significance.’ Richard Davenport-Hines, Times Literary Supplement Books of the Year, 2021 </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">'Akam’s beautifully written, from the inside out, account of the British Army’s reluctance to engage with the realities of recent small wars, in Afghanistan in particular, is a must-read for every serious student of modern military history. At one level, it explains how and why we managed to turn victory over al-Qaeda in Afghanistan into defeat at the hands of the Taliban. But this book is about much more than the army in Afghanistan – it is a parable about failure, the failure of a revered institution, with a proud history and an uncritical public, to come to terms with a changed and changing world.' Sir Sherard Cowper-Coles, former British Ambassador to Afghanistan. </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">'Simon Akam delivers a devastating indictment of Britain’s military chiefs for overseeing the shocking decline of the nation’s armed forces. His book is compulsory reading for every patriot.' Tom Bower, biographer. </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">'Simon Akam has written a perceptive, challenging and passionate book that looks at modern soldiering. In doing so, Akam provides an invaluable look at how the British Army works – and how the changing world in the 21st century is asking new and complex questions for soldiers and military strategy alike.' Peter Frankopan, author, The Silk Roads. </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">'This brave, absorbing and prodigiously well-researched tour de force renders every previous account of the British Army in its disastrous recent campaigns obsolete. Akam makes an unanswerable case that we are no longer very good at fighting wars, building his arguments with panache and good sense. In doing so he has done his country, and the army, a great service - although the Generals may not see it quite that way just yet. Put away the self-serving autobiographies and the obsequious histories of in-house academics; this is the definitive account of the British Army in its 21st century misadventures.' Frank Ledwidge, author, Losing Small Wars.  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">'[An] excellent and valuable book.' Jason Burke, The Guardian  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A powerful, compelling, and fascinating polemic.’ William Boyd.  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Many will disagree with Akam’s conclusions, but it is important that arguments like his are given a proper airing ... I got through all 500-plus pages in two sittings — and it is certainly worth the effort.’ Adrian Weale, Mail on Sunday </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A valuable addition to analysing the past, present, and future of a venerated institution.’ The Independent </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Full of gripping reportage and compelling personal stories.’ TLS </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Persuasive.’ Matthew Parris, The Times  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Detailed and well structured.’ Anthony Loyd, New Statesman  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A passionate book.’ Max Hastings, The Sunday Times </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Akam is an angry young man and the book is better for it.’ Roger Boyes, The Times  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A brilliant book ... Gets right to the heart of so many of the British Army’s problems.’ Simon Scarrow, Sunday Times bestselling author of The Eagles of the Empire series </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A timely, elegant, and important book.’ David Patrikarakos, The Spectator  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘The military would obviously like to avoid a close examination of this unbroken string of catastrophes, but Akam’s book is a gentle account — critical, but not unsympathetic.’ Tom Stevenson, London Review of Books  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A blistering account ... formidable.’ Richard Norton-Taylor, Declassified UK  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Looks at the changes the British Army has undergone and roles it has played as an almost volunteer sidekick to the American military in the war on terror.’ C.J. Chivers, The New York Times  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘A scathing account of the British Army in the years after 9/11 ... full of forensic detail — and delicious gossip.’ Shashank Joshi, The Spectator  </span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">‘Makes many important points.’ Helen Parr, Prospect </span></p><p style=\"line-height: 1;\"><br></p><p><span style=\"font-size: 14px;\">‘Beautifully written, evocative and passionate prose ... [The] breadth of research gives Akam’s book immense power.’ Nicholas Stuart, The Canberra Times </span></p><p><br></p><p><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\"><span class=\"ql-cursor\"></span></span></p>","textWidth":77}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false},{"id":1754870441132,"title":"Always Take Notes","isFolder":true,"isCollapsed":true,"visible":true,"parentId":null,"siteId":"btfkih7o","slug":"always-take-notes","importSource":"folder","isSubmenu":true,"isHomePage":false},{"id":1754870441133,"title":"Podcast","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1754870441133","visible":true,"parentId":1754870441132,"siteId":"btfkih7o","slug":"podcast","url":"/podcast","pageElements":[{"id":1754870442133,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Podcast","metaDescription":"","metaKeywords":""}
1539,{"id":1754871910450,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754870444133,"type":"column-container","title":"Two Column Layout","visible":true,"position":2,"columns":[{"id":1754870444433,"hAlign":"center","vAlign":"top","elements":[{"id":1754870444233,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754871099230_ATN_SQUARE-LOGO_COL_300dpi.jpg","imageKey":"btfkih7o/1754871099230_ATN_SQUARE-LOGO_COL_300dpi.jpg","imageFilename":"ATN_SQUARE-LOGO_COL_300dpi.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1754870444533,"hAlign":"left","vAlign":"middle","elements":[{"id":1754870444333,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">Co-hosted by Simon Akam and </span><a href=\"https://x.com/rachelsllloyd\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Rachel Lloyd</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">, </span><a href=\"https://www.alwaystakenotes.com/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Always Take Notes</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> is a podcast for and about writers and writing that aims to be informative (tackling vexing questions including how to negotiate fees, what makes an excellent book pitch and how to write for TV). We aim for a deliberately broad church, and past guests have included historians Peter Frankopan, Antony Beevor, Lucy Hughes-Hallett, Niall Ferguson and Max Hastings, novelists Anne Enright, Mark Haddon, Ian Rankin, Joanne Harris and Jeffrey Archer, journalists Sirin Kale, Patrick Radden Keefe and Hadley Freeman, and the editors of the Financial Times, the Times Literary Supplement, Cosmopolitan, Hello! and British GQ. The podcast is on iTunes </span><a href=\"https://itunes.apple.com/gb/podcast/always-take-notes/id1224996246?mt=2\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">here</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">; we are also on </span><a href=\"https://twitter.com/takenotesalways\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Twitter</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> and </span><a href=\"https://www.instagram.com/alwaystakenotes/?hl=en\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Instagram</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">. Our crowd funding page is on Patreon </span><a href=\"http://patreon.com/alwaystakenotes\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">here</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">.</span></p>","textWidth":86}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50}}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false},{"id":1754870441134,"title":"Always Take Notes book","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1754870441134","visible":true,"parentId":1754870441132,"siteId":"btfkih7o","slug":"always-take-notes-book","url":"/always-take-notes-book","pageElements":[{"id":1754870442134,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Always Take Notes book","metaDescription":"","metaKeywords":""},{"id":1754872026429,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754870444134,"type":"column-container","title":"Two Column Layout","visible":true,"position":2,"columns":[{"id":1754870444434,"hAlign":"center","vAlign":"top","elements":[{"id":1754870444234,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754871141776_ATN-insta-square.jpg","imageKey":"btfkih7o/1754871141776_ATN-insta-square.jpg","imageFilename":"ATN-insta-square.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1754870444534,"hAlign":"left","vAlign":"middle","elements":[{"id":1754870444334,"type":"text","t
1539itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.hatchards.co.uk/book/always-take-notes/simon-akam/9781804183182\" target=\"_blank\" style=\"font-size: 14px; color: rgb(89, 89, 89);\">Always Take Notes: Advice from some of the world's greatest writers</a></p><p><span style=\"color: rgb(89, 89, 89);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(89, 89, 89);\">Bestselling and award-winning authors share the secrets to their success, and the hard lessons they have learnt along the way.</span></p><p><span style=\"color: rgb(89, 89, 89);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(89, 89, 89);\">Where do the best ideas come from? How do you stay motivated? What does it take to become a published author? And how do you actually make money from your writing? For over five years the hosts of Always Take Notes podcast have posed their nosiest questions to some of the world's greatest writers. The result is a compendium of frank and frequently entertaining guidance for living a creative life. From the early failures that shaped them to the daily challenges of writing and the habits that keep them on track, literary luminaries offer guidance to inspire.</span></p><p><span style=\"color: rgb(89, 89, 89);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(89, 89, 89);\">Featuring:</span></p><p><span style=\"color: rgb(89, 89, 89);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(89, 89, 89);\">Alexander McCall Smith, Anne Enright, Candice Carty-Williams, Christina Lamb, Colin Thubron, Colum McCann, David Mitchell, Elif Shafak, George Packer, Hadley Freeman, Hollie McNish, Ian McEwan, Ian Rankin, Irvine Welsh, Jeffrey Archer, Joanne Harris, Kate Mosse, Kiran Millwood Hargrave, Kit de Waal, Louise Doughty, Lucy Hughes-Hallett, Maggie Fergusson, Mark Haddon, Marlon James, Max Hastings, May Jeong, Merve Emre, Monica Ali, Niall Ferguson, Nikesh Shukla, Oliver Bullough, Orlando Figes, Patrick Kingsley, Rory Stewart, Rosie Nixon, Ruth Ozeki, Ruth Padel, Sam Knight, Samanth Subramanian, Samira Shackle, Sara Baume, Sebastian Junger, Simon Lancaster, Simon Scarrow, Stig Abell, Terri White, Tessa Hadley, Tim Rice, Toby Young, Tracy Chevalier, William Boyd, William Dalrymple, and many more...</span></p><p><span style=\"color: rgb(89, 89, 89);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(89, 89, 89);\">Publisher: Bonnier Books Ltd</span></p><p><span style=\"color: rgb(89, 89, 89);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(89, 89, 89);\">ISBN: 9781804183182</span></p>","textWidth":86}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false},{"id":1754870441135,"title":"Longform journalism","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1754870441135","visible":true,"parentId":null,"siteId":"btfkih7o","slug":"longform-journalism","url":"/longform-journalism","pageElements":[{"id":1754870442135,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Longform journalism","metaDescription":"","metaKeywords":""},{"id":1754872000818,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754870444135,"type":"column-container","title":"Two Column Layout","visible":true,"position":2,"columns":[{"id":1754870444435,"hAlign":"center","vAlign":"top","elements":[{"id":1754870444235,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754871269819_1117_FEA_Scotland-1---RGB.jpg","
1539imageKey":"btfkih7o/1754871269819_1117_FEA_Scotland-1---RGB.jpg","imageFilename":"1117_FEA_Scotland-1---RGB.jpg","imageType":"image/jpeg"},{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754871268206_Bizweek-clerks.jpg","imageKey":"btfkih7o/1754871268206_Bizweek-clerks.jpg","imageFilename":"Bizweek-clerks.jpg","imageType":"image/jpeg"},{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754871266106_Simon-1.jpg","imageKey":"btfkih7o/1754871266106_Simon-1.jpg","imageFilename":"Simon-1.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":94,"slideshowHeight":73,"showFullImages":true}]},{"id":1754870444535,"hAlign":"left","vAlign":"middle","elements":[{"id":1754870444335,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"margin-bottom: 0.5em;\"><a href=\"https://www.vanityfair.com/news/story/glock-guns-family-feud?srsltid=AfmBOopnsCY0ku5BwsqMhfNWoENnJPyGwHThtDqS6S-Zh9dJO9biS0wK\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Kiss Kiss Bang Bang,' Vanity Fair, published 2 April 2026</a></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><a href=\"https://www.newyorker.com/magazine/2025/12/08/a-very-big-fight-over-a-very-small-language\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'A Very Big Fight Over a Very Small Language,' The New Yorker, published 1 December 2025</a></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2024/05/13/why-is-britain-hopeless-at-punishing-corruption\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Why is Britain hopeless at punishing corruption?,' Economist 1843, published 13 May 2024</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theguardian.com/sport/2024/jan/30/precipice-of-fear-the-freerider-who-took-skiing-to-its-limits\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Precipice of fear: the freerider who took skiing to its limits,' Guardian Long Read, published 30 January 2024</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newyorker.com/sports/sporting-scene/the-man-who-completed-football\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Man Who “Completed Football”,' the New Yorker, 10 November 2023</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2023/01/29/bypassed-are-heart-surgeons-dying-out\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Bypassed: are heart surgeons dying out?,' Economist 1843, published 29 January 2023</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2022/04/14/prince-harrys-games-does-the-olympics-for-veterans-help-them-recover\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Prince Harry’s games: does the Olympics for veterans help them recover?,' Economist 1843, published 14 April 2022</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.lrb.co.uk/the-paper/v44/n05/simon-akam/a-surfeit-of-rank\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'A Surfeit of Rank,' the London Review of Books, 10 March, 2022</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newyorker.com/science/elements/a-new-story-for-stonehenge\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'A New Story for Stonehenge,' the New Yorker, 15 February, 2022</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theglobeandmail.com/arts/books/article-excerpt-in-the-c
1539hanging-of-the-guard-simon-akam-takes-british-army-to/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Excerpt: In The Changing of the Guard, Simon Akam takes British Army to task for failing to learn from failure,' The Globe and Mail, 30 July, 2021</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.runnersworld.com/uk/health/injury/a37128768/history-of-blisters/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'There’s the rub: the painful history of the blister,' Runner's World, published 26 July, 2021</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.outsideonline.com/outdoor-adventure/exploration-survival/crystal-hunters-chamonix-mont-blanc-france/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Crystal Hunters of Chamonix,' Outside, published 8 June, 2021</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.gq-magazine.co.uk/politics/article/marine-a\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'War, justice and the real story behind ‘Marine A,’ British GQ, published 3 April, 2021</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2021/03/26/a-queue-of-ambulances-a-handful-of-beds-everyone-looked-to-her-for-solutions\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'A queue of ambulances. A handful of beds. Everyone looked to her for solutions,' Economist 1843, published 26 March, 2021</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.ft.com/content/468d42c9-c107-4518-9716-ca841076af30\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Line of fire: what it is to be a man in today’s British army,' Financial Times magazine, published 19 February, 2021 </a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2020/11/26/on-the-hunt-with-yorkshires-virus-detectives\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'On the hunt with Yorkshire’s virus-detectives,' Economist 1843, published 26 November, 2020</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2020/07/26/the-inside-story-of-britains-fight-against-covid-19\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The inside story of Britain’s fight against covid-19,' Economist 1843, published 26 July, 2020</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theguardian.com/tv-and-radio/2020/feb/04/magic-radio-indestructible-easy-listening-streaming\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Magic moments: the indestructible appeal of easy listening radio,' Guardian Long Read, published 4 February, 2020</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.bloomberg.com/news/features/2020-03-06/the-awkward-ambition-of-elite-british-private-schools-abroad?sref=8XGseOCd\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Awkward Ambition of Elite British Private Schools Abroad,' Bloomberg Businessweek, published 6 March, 2020</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.outsideonline.com/2405575/chamonix-mountain-guide-test-probatoire\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'You Won't Pass Chamonix's Mountain-Guiding Test,' Outside, published 27 November, 2019</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.economist.com/1843/2019/11/18/death-in-the-alps\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Death in the Alps, Economist 1843, published 18 November, 2019</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.bloomberg.com/news/features/2019-07-19/chronicles-of-the-last-quizmaster-in-the-age-of-big-trivia?sref=8XGseOCd\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Chronicles of the Last Quizmaster in the Age of Big Trivia,' Bloomberg Businessweek, published 19 July, 2019</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.the-tls.co.uk/articles/lord-alfred-douglas-bosie-reputational-rehab/?utm_medium=Social&amp;utm_source=Twitter#Echobox=1542994620\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Bosie in reputational rehab,' the Times Literary Supplement, published 23 November, 2018</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://bleacherreport.com/articles/2780765-the-belgium-process\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Belgium Process,' Bleacher Report, published 13 June, 2018</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theparisreview.org/blog/2018/02/23/portrait-of-a-friendship/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Portrait of a friendship,' the Paris Review, published 23 February, 2018</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.outsideonline.com/2254791/braveheart-couldnt-handle\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Braveheart Couldn't Handle This,' Outside, published 26 October, 2017</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://bleacherreport.com/articles/2735519-youth-footballer-premier-league-clubs-recruiting\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Inside the impossible dream of a six-year-old footballer,' Bleacher Report, published 28 September, 2017</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.bloomberg.com/news/features/2017-05-23/the-exquisitely-english-and-amazingly-lucrative-world-of-london-clerks\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Exquisitely English (and Amazingly Lucrative) World of London Clerks,' Bloomberg Businessweek, published 23 May, 2017</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://bleacherreport.com/articles/2708890-a-dream-that-died-on-the-pitch-the-tragic-story-of-patrick-ekeng\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'A Dream That Died on the Pitch: The Tragic Story of Patrick Ekeng,' Bleacher Report, published 11 May, 2017</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.gq-magazine.co.uk/article/viola-beach\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The short life and mysterious death of Viola Beach,' British GQ, published 27 June, 2016</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theguardian.com/business/2016/mar/15/british-umpire-how-institute-fiscal-studies-became-most-influential-voice-in-uk-economic-debate\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The British umpire: how the IFS became the most influential voice in the economic debate,' Guardian Long Read, published 15 March, 2016 </a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newsweek.com/londons-terror-protocols-plan-next-77-349008\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'London's Terror Protocols: The Plan for the Next Terrorism Attack,' Newsweek, published 1 July, 2015</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newsweek.com/big-tobacco-fights-back-how-cigarette-kings-bought-vaping-industry-336432\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Big Tobacco Fights Back: How the Cigarette Kings Bought the Vaping Industry,' Newsweek, published 27 May, 2015</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newsweek.com/2014/12/26/murky-side-beautiful-game-life-bottom-english-football-292387.html\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">‘The Murky Side of the Beautiful Game: Life at the Bottom of English Football,’ Newsweek, published 22 December, 2014</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newsweek.com/2014/11/07/physical-and-mental-trauma-souvenirs-battle-afghanistan-280405.html\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">‘Souvenirs of the War in Afghanistan: Many Troops Return With Physical and Mental Trauma,’ Newsweek, published 28 October, 2014</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newsweek.com/2014/10/03/knife-games-weird-weekend-worlds-top-plastic-surgeons-273439.html\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">‘Knife Games: A Weird Weekend With the World's Top Plastic Surgeons,’ Newsweek, published 26 September, 2014</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newsweek.com/2014/09/12/scottish-referendum-two-friends-journey-heart-independence-debate-268158.html\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">‘Scottish Referendum: Two Friends Journey to the Heart of the Independence Debate,’ Newsweek, published 4 September, 2014</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theparisreview.org/blog/2013/10/10/the-diary-diaries/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Diary Diaries,' the Paris Review, published 10 October, 2013</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.theparisreview.org/blog/2013/04/01/on-the-occasion-of-the-removal-of-my-girlfriends-dogs-balls/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'On the Occasion of the Removal of My Girlfriend’s Dog’s Balls, the Paris Review, published 1 April, 2013</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newstatesman.com/world-affairs/world-affairs/2012/05/letter-dakar\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Letter from Dakar,' the New Statesman, published 16 May, 2012</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newstatesman.com/africa/2012/01/sierra-leone-strasser-war\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The vagabond king,' the New Statesman, published 2 February, 2012</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://newrepublic.com/article/95493/prince-johnson-liberia\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'The Comeback: A notorious ex-warlord hits the campaign trail in Liberia,' the New Republic, published 28 September, 2011</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"https://www.newstatesman.com/society/2009/06/native-american-gaming\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'Old wound, same pain,' the New Statesman, published 11 June, 2009</a></p><p><span style=\"color: rgb(69, 69, 69); font-size: 14px; font-family: Helvetica, sans-serif;\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"mailto:https://www.newstatesman.com/north-america/2008/10/virginia-state-vote-obama\" target=\"_blank\" style=\"font-size: 14px; color: rgb(69, 69, 69); font-family: Helvetica, sans-serif;\">'\"We’re still fighting the Civil War here,\"' the New Statesman, published 
153930 October, 2008</a></p>","textWidth":88}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50}}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false},{"id":1754870441136,"title":"About","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1754870441136","visible":true,"parentId":null,"siteId":"btfkih7o","slug":"about","url":"/about","pageElements":[{"id":1754870442136,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"About","metaDescription":"","metaKeywords":""},{"id":1754871895088,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754870444136,"type":"column-container","title":"Two Column Layout","visible":true,"position":2,"columns":[{"id":1754870444436,"hAlign":"center","vAlign":"top","elements":[{"id":1754870444236,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/btfkih7o/1754871868012_akam_reed_headshot.jpg","imageKey":"btfkih7o/1754871868012_akam_reed_headshot.jpg","imageFilename":"akam_reed_headshot.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1754870444536,"hAlign":"left","vAlign":"middle","elements":[{"id":1754870444336,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">Simon Akam (</span><a href=\"https://x.com/simonakam\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">@simonakam)</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> is a British magazine journalist and author. His writing has appeared in publications including the New Yorker, Vanity Fair, the London Review of Books, The Economist's 1843 magazine, the Guardian Long Read, Outside, the New York Times, GQ and Bloomberg Businessweek. Simon’s first book, The Changing of the Guard – the British Army since 9/11, was published in 2021 to widespread acclaim. It won the Templer First Book Prize and the Times Literary Supplement, choosing a book of the year, described \"A state-of-the-nation book of resounding power, deep conviction and far-reaching significance.\" Simon’s second book, Snowlines, will be published by HarperCollins in the UK and Grove Atlantic in the US in early 2027. Simon also co-hosts the writing podcast </span><a href=\"https://www.alwaystakenotes.com/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Always Take Notes</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> with Rachel Lloyd.</span></p><p><span style=\"color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">Photograph taken by </span><a href=\"http://www.dudleyreed.com/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Dudley Reed</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> while a fellow at on the </span><a href=\"https://logannonfiction.org/\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">Logan Nonfiction Programme</a><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> at the Carey Institute for Global Good in upstate New York.</span></p>","textWidth":88}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50}}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false},{"id":1754870441137,"title":"Contact","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1754870441137","visible":true,"parentId":null,"siteId":"btfkih7o","slug":"contact","url":"/contact","pageElements":[{"id":1754871980822,"type":"text","t
1539itle":"Text Block","visible":true,"position":0,"textContent":"<p>  </p><p><br></p>","textWidth":100},{"id":1754870442038,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">Email </span><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">[email protected]</a></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><strong style=\"font-size: 14px; color: rgb(92, 92, 92);\">Representation</strong><span style=\"font-size: 14px; color: rgb(92, 92, 92);\"> Patrick Walsh</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">PEW Literary Agency Limited</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">46 Lexington Street</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">Soho, London W1R OLP</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">+44 (0) 20 7734 4464</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">[email protected]</a></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(92, 92, 92);\">\n</span></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"font-size: 14px; color: rgb(92, 92, 92);\">[email protected]</a></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><br></p>","textWidth":31},{"id":1754871910229,"type":"metadata","visible":true,"position":2,"metaTitle":"","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0737e49","importSource":"classic-page","isHomePage":false,"position":10}];
1540window.galleries = galleries;
1541
1542function getSiteGalleriesArray() {
1543  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
1544    return galleries;
1545  }
1546  if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
1547    return window.galleries;
1548  }
1549  if (window.GALLERY_DATA_CACHE &&
1550      window.GALLERY_DATA_CACHE.initialized &&
1551      Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
1552    return window.GALLERY_DATA_CACHE.galleries;
1553  }
1554  return [];
1555}
1556window.getSiteGalleriesArray = getSiteGalleriesArray;
1557
1558let activeGalleryId = null;
1559let sortableInstance = null;
1560let isEditing = false;
1561let initialPageInfo = {"id":1754870441133,"type":"page","pageId":"page_1754870441133","slug":"podcast","url":"/podcast"};
1562
1563
1564// Store the current menu layout type (must be JSON-encoded — bare sidebar/horizontal is invalid JS)
1565let currentMenuLayout = null;
1566
1567/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */
1568function isNeonPageEmbedShell() {
1569  try {
1570    if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed'))
1571      return true;
1572    var _spEmbed = new URLSearchParams(window.location.search);
1573    return _spEmbed.get('neon_embed') === '1';
1574  } catch (_eEmbed) { return false; }
1575}
1576
1577/**
1578 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing),
1579 * otherwise use each gallery/page flag.
1580 */
1581function shouldApplyMenuHiddenForEmbedOrGallery(gallery) {
1582  var editing = typeof window.isInEditMode === 'function' && window.isInEditMode();
1583  if (editing)
1584    return !!(gallery && gallery.hideMenuOnPage);
1585  return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell();
1586}
1587
1588function shouldApplyMenuOverlayOnPage(gallery) {
1589  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery))
1590    return false;
1591  return !!(gallery && gallery.overlayMenuOnPage);
1592}
1593
1594function isOverlayMenuChromeActive() {
1595  return document.body.classList.contains('menu-overlay-on-page')
1596    && !document.body.classList.contains('edit-mode-active');
1597}
1598
1599/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */
1600function syncOverlayMenuInlineChrome() {
1601  var sidebar = document.querySelector('.sidebar');
1602  var header = document.querySelector('.mobile-header');
1603  if (isOverlayMenuChromeActive()) {
1604    if (sidebar) {
1605      sidebar.style.setProperty('background-color', 'transparent', 'important');
1606      sidebar.style.setProperty('background-image', 'none', 'important');
1607    }
1608    if (header) {
1609      header.style.setProperty('background-color', 'transparent', 'important');
1610      header.style.setProperty('background-image', 'none', 'important');
1611    }
1612    return;
1613  }
1614  if (sidebar) {
1615    var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa';
1616    var hidden = document.body.classList.contains('menu-background-hidden');
1617    sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important');
1618    if (hidden) {
1619      sidebar.style.setProperty('background-image', 'none', 'important');
1620    } else {
1621      sidebar.style.removeProperty('background-image');
1622    }
1623  }
1624  if (header) {
1625    header.style.removeProperty('background-color');
1626    header.style.removeProperty('background-image');
1627  }
1628}
1629
1630function applyMenuOverlayClassForGallery(gallery) {
1631  if (shouldApplyMenuOverlayOnPage(gallery)) {
1632    document.body.classList.add('menu-overlay-on-page');
1633  } else {
1634    document.body.classList.remove('menu-overlay-on-page');
1635  }
1636  syncOverlayMenuInlineChrome();
1637}
1638
1639function hookMenuStyleCustomizerForOverlay() {
1640  var customizer = window.MenuStyleCustomizer;
1641  if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function')
1642    return;
1643  var original = customizer.applyStyles.bind(customizer);
1644  customizer.applyStyles = function() {
1645    original.apply(this, arguments);
1646    if (isOverlayMenuChromeActive())
1647      syncOverlayMenuInlineChrome();
1648  };
1649  customizer.__overlayBgHooked = true;
1650}
1651window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery;
1652
1653document.addEventListener('DOMContentLoaded', () => {
1654  if (window.hydraInitialized) {
1655    console.log("Hydra already initialized, skipping");
1656    return;
1657  }
1658  window.hydraInitialized = true;
1659  document.body.classList.add('hydra-initialized');
1660  try {
1661    if (sessionStorage.getItem('hydra_refresh_cleared') === '1') {
1662      sessionStorage.removeItem('hydra_refresh_cleared');
1663      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
1664        window.EnhancedGallerySystem.resetInitializationState();
1665      }
1666      console.log('[Hydra] Applied ?refresh=1 — gallery framework reset for clean load.');
1667    }
1668  } catch (e) {}
1669  try {
1670    var neonEmbedParams = new URLSearchParams(window.location.search);
1671    if (neonEmbedParams.get('neon_embed') === '1') {
1672      document.body.classList.add('neon-page-embed', 'menu-hidden-on-page');
1673    }
1674  } catch (_neonEmbedErr) {}
1675  hookMenuStyleCustomizerForOverlay();
1676  setTimeout(hookMenuStyleCustomizerForOverlay, 0);
1677  setTimeout(hookMenuStyleCustomizerForOverlay, 500);
1678  if (isOverlayMenuChromeActive())
1679    syncOverlayMenuInlineChrome();
1680  window.Parameters = window.Parameters || {};
1681  if (localStorage.getItem('hydra_is_admin') === 'true') {
1682    document.body.classList.add('hydra-admin', 'hydra-authenticated');
1683    isAdmin = true;
1684    isAuthenticated = true;
1685    console.log('Restored admin status from localStorage');
1686    
1687    // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins
1688    // This ensures admins always see fresh data when opening new tabs
1689    try {
1690      console.log('Admin detected - clearing all caches except auth tokens...');
1691      
1692      // Save auth tokens first
1693      const savedAuthToken = localStorage.getItem('hydra_auth_token');
1694      const savedAuthEmail = localStorage.getItem('hydra_auth_email');
1695      const savedIsAdmin = localStorage.getItem('hydra_is_admin');
1696      const savedDidToken = localStorage.getItem('hydra_did_token');
1697      
1698      // Clear ALL localStorage
1699      localStorage.clear();
1700      
1701      // Restore auth tokens
1702      if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken);
1703      if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail);
1704      if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin);
1705      if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken);
1706      
1707      // Clear ALL sessionStorage (auth tokens are not stored here)
1708      sessionStorage.clear();
1709      
1710      console.log('Cleared all caches except auth tokens for admin user');
1711    } catch (cacheError) {
1712      console.warn('Error clearing caches on admin page load:', cacheError);
1713      // Don't fail page load if cache clearing fails
1714    }
1715  }
1716
1717  function ensureSidebarElementsAndRender() {
1718  console.log('Ensuring sidebar elements are available before rendering...');
1719  
1720  // Restore from siteConfig when hydrate missed or init invented a default menu
1721  if (window.SidebarManager) {
1722    var current = window.SidebarManager.elements;
1723    var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements;
1724    if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) {
1725      console.log('Initializing SidebarManager elements from siteConfig');
1726      window.SidebarManager.elements = siteConfig.sidebarElements;
1727      window.SidebarManager._elementsReady = true;
1728      window.SidebarManager._createdDefaultElements = false;
1729      if (typeof window.SidebarManager.renderElements === 'function') {
1730        window.SidebarManager.renderElements();
1731      }
1732    }
1733  }
1734  
1735  // Determine layout and render
1736  const currentMenuLayout = getEffectiveMenuLayout();
1737  
1738  if (currentMenuLayout === 'horizontal') {
1739    console.log('Rendering horizontal menu with ensured sidebar elements');
1740    renderHorizontalMenu();
1741  } else {
1742    renderGalleries();
1743  }
1744}
1745
1746 setTimeout(() => {
1747  console.log("Initializing galleries:", galleries.length);
1748  // Collapse folders before first paint so nested items never flash as expanded plain text
1749  alignFolderStates();
1750  
1751  // CHANGED: Use the new function instead of direct rendering
1752  setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init
1753  
1754  initMobileMenu();
1755  
1756  /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */
1757  let bootstrapRouteAttempted = false;
1758
1759  // Handle direct URL navigation
1760  if (initialPageInfo) {
1761    console.log("Loading initial page from direct URL:", initialPageInfo);
1762    
1763    // Find the gallery by ID
1764    const gallery = galleries.find(g => g.id === initialPageInfo.id);
1765    
1766    if (gallery) {
1767      // Set active gallery ID
1768      activeGalleryId = gallery.id;
1769      
1770      if (initialPageInfo.type === 'page') {
1771        console.log("Loading page directly:", gallery.title);
1772        loadPage(gallery.id);
1773      } else {
1774        console.log("Loading gallery directly:", gallery.title);
1775        loadGallery(gallery.id);
1776      }
1777      bootstrapRouteAttempted = true;
1778    }
1779  } else if (window.location.pathname === '/') {
1780    // No specific path, try to load home page
1781    console.log("No specific path, checking for home page");
1782    if (!loadHomePage() && galleries.length > 0) {
1783      // Default to first gallery if no home page is set
1784      loadGallery(galleries[0].id);
1785    }
1786    bootstrapRouteAttempted = true;
1787  } else if (galleries.length > 0) {
1788    // Path exists but no matching initialPageInfo, try to handle URL path
1789    handleURLNavigation();
1790    bootstrapRouteAttempted = true;
1791  }
1792  window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted;
1793  
1794  // Log what was rendered
1795  console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found");
1796  
1797    // Start with edit controls properly set
1798    toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active'));
1799    
1800    // Explicitly hide the import classic form too
1801    const importForm = document.getElementById('importClassicForm');
1802    if (importForm) {
1803      importForm.style.display = 'none';
1804      importForm.classList.remove('visible');
1805    }
1806    
1807    // Try to restore token from localStorage
1808    if (!didToken) {
1809      didToken = localStorage.getItem('hydra_auth_token');
1810      if (didToken) {
1811        console.log('Restored auth token from localStorage');
1812      }
1813    }
1814    
1815    // Initialize Magic and check authentication
1816    
1817    
1818    // Initialize sidebar elements if available
1819    if (window.SidebarManager) {
1820      // Pass the initial elements from server config if available
1821      if (true) {
1822        window.SidebarManager.elements = [{"id":1754867526798,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1754872106425,"type":"text","title":"Text","visible":true,"position":0,"textContent":"<p>Simon Akam</p>"},{"id":1754877522531,"type":"social","title":"Social","visible":true,"position":3,"socialIconSize":20,"socialIcons":[{"type":"facebook","url":"https://facebook.com/simon.akam"},{"type":"x","url":"https://twitter.com//simonakam"},{"type":"instagram","url":"https://instagram.com/simon.akam"},{"type":"linkedin","url":"https://linkedin.com/in/simonakam/"}],"socialAlignment":"left"},{"id":1754877610513,"type":"image","title":"Image","visible":true,"position":2,"imageData":"https://storage.neonsky.app/btfkih7o/1754877628319_spacer.gif","imageWidth":"26%","imageAlignment":"center"}];
1823        if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
1824          window.SidebarManager._elementsReady = true;
1825          window.SidebarManager._createdDefaultElements = false;
1826        }
1827      }
1828      window.SidebarManager.init();
1829    }
1830
1831
1832    // Initialize metadata from server config (site-wide only — not current page SEO)
1833window.siteMetadata = {"title":"Simon Akam","description":"Email [email protected] · Representation Patrick Walsh","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":""};
1834console.log('Client Initialized site metadata (second init):', window.siteMetadata);
1835console.log('Client Google Analytics from metadataToUse (second init):', '');
1836console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics);
1837console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage);
1838
1839    // Update copyright footer after metadata is set (important for main domain)
1840    if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
1841      window.SidebarManager.updateMetadataFooter();
1842    }
1843
1844
1845
1846    const submenuCheck = document.getElementById('createSubmenu');
1847    const submenuTitleField = document.getElementById('submenuTitle');
1848    const submenuTitleGroup = submenuTitleField?.parentElement;
1849    
1850    if (submenuCheck && submenuTitleGroup) {
1851      submenuCheck.addEventListener('change', function() {
1852        submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
1853      });
1854    }
1855    
1856    // Check authentication state after initialization
1857    if (isAuthenticated && isAdmin) {
1858      console.log('Already authenticated and admin, showing edit UI');
1859      forceShowEditUI();
1860    } else if (didToken) {
1861      console.log('Have token, checking admin status');
1862      checkAuth();
1863    }
1864  }, 100); // Small delay to ensure everything is loaded
1865
1866  setTimeout(() => {
1867    if (window.location.pathname === '/' ||
1868        window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' ||
1869        (window.location.hostname === 'preview.neonsky.app' && 
1870         window.location.pathname.split('/').length <= 2)) {
1871      // Root URL or preview URL with only GUID - check for home page
1872      // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load)
1873      if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) {
1874        loadHomePage();
1875      }
1876    }
1877  }, 500);
1878
1879  // Dedicated editor URL: open edit tools or OTP login without requiring + key
1880  setTimeout(function hydraManageEditorEntry() {
1881    var p = window.location.pathname;
1882    if (p !== '/_manage' && p !== '/_manage/') return;
1883    try {
1884      if (sessionStorage.getItem('hydra_logout_cleared') === '1') {
1885        sessionStorage.removeItem('hydra_logout_cleared');
1886        console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.');
1887        return;
1888      }
1889    } catch (e) {}
1890    if (document.body.classList.contains('edit-mode-active')) return;
1891    try {
1892      var token = window.didToken || localStorage.getItem('hydra_auth_token');
1893      var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true';
1894      if (token && admin) {
1895        if (typeof window.toggleEditMode === 'function') window.toggleEditMode();
1896        return;
1897      }
1898      if (token && typeof checkAdminStatus === 'function') {
1899        checkAdminStatus(token).then(function() {
1900          if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) {
1901            window.toggleEditMode();
1902          }
1903        }).catch(function() {});
1904        return;
1905      }
1906      if (typeof startOTPLogin === 'function') {
1907        var pr = startOTPLogin();
1908        if (pr && typeof pr.catch === 'function') pr.catch(function() {});
1909      }
1910    } catch (e) {
1911      console.error('hydraManageEditorEntry:', e);
1912    }
1913  }, 2200);
1914});
1915
1916/**
1917 * Accessible name for header/sidebar brand images (no visual change).
1918 * Prefers element title or site metadata over generic "Image" / filenames.
1919 */
1920function resolveBrandLogoAlt(element) {
1921  if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') {
1922    return window.SidebarManager._resolveBrandImageAlt(element || {});
1923  }
1924  const siteTitle =
1925    (window.siteMetadata && String(window.siteMetadata.title || '').trim()) ||
1926    (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') ||
1927    String(location.hostname || '').replace(/^www\./i, '');
1928  const title = element && element.title ? String(element.title).trim() : '';
1929  if (title && !/^image(\s*\d*)?$/i.test(title)) return title;
1930  return siteTitle || 'Site logo';
1931}
1932
1933/**
1934 * Gets the first visible sidebar element (image or text) to be used as a logo or title.
1935 * @returns {object|null} The first suitable sidebar element or null.
1936 */
1937
1938function getFirstSidebarElementForHeader() {
1939    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
1940        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
1941        if (visibleElements.length > 0) {
1942            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
1943            
1944            // Debug: Log all element types found
1945            const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title }));
1946            console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes);
1947            
1948            for (let el of sortedElements) {
1949                if (el.type === 'image' || el.type === 'text') { // Filter 2: type
1950                    console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId });
1951                    return el;
1952                }
1953            }
1954            // If loop finishes, no visible image or text found among visible elements
1955            console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type));
1956            return null;
1957        } else {
1958            console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length);
1959            return null;
1960        }
1961    }
1962    console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
1963    return null;
1964}
1965
1966/**
1967 * Gets the first visible social icon element from sidebar elements.
1968 * @returns {object|null} The first suitable social element or null.
1969 */
1970function getFirstSocialElementForHeader() {
1971    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
1972        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
1973        if (visibleElements.length > 0) {
1974            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
1975            for (let el of sortedElements) {
1976                if (el.type === 'social') { // Filter for social type
1977                    return el;
1978                }
1979            }
1980            // If loop finishes, no visible social element found
1981            console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.');
1982            return null;
1983        } else {
1984            console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.');
1985            return null;
1986        }
1987    }
1988    console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
1989    return null;
1990}
1991
1992function isSpaceColonySiteTheme() {
1993  const themeId = document.body && document.body.dataset.siteTheme;
1994  return themeId === 'space-colony';
1995}
1996
1997/**
1998 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu.
1999 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked.
2000 */
2001function applyHorizontalLogoMenuLayoutStyles() {
2002  const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings;
2003  const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline';
2004  const isSpaceColonyTheme = isSpaceColonySiteTheme();
2005  document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked');
2006  const w = document.querySelector('.logo-menu-container');
2007  if (w) {
2008    const isMobile = window.innerWidth <= 768;
2009    w.style.display = 'flex';
2010    w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center';
2011    if (mode === 'stacked') {
2012      w.style.flexDirection = 'column';
2013      w.style.gap = isMobile ? '8px' : '12px';
2014      w.style.width = '100%';
2015      w.style.boxSizing = 'border-box';
2016      w.style.flexGrow = isMobile ? '0' : '1';
2017      w.style.minWidth = '0';
2018      w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start';
2019    } else {
2020      w.style.flexDirection = 'row';
2021      w.style.gap = '20px';
2022      w.style.width = '';
2023      w.style.boxSizing = '';
2024      w.style.minWidth = '';
2025      if (isMobile) {
2026        w.style.flexGrow = '0';
2027        w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
2028      } else {
2029        w.style.flexGrow = '';
2030        w.style.justifyContent = '';
2031      }
2032    }
2033  }
2034  const mhc = document.querySelector('.mobile-header-content');
2035  if (mhc) {
2036    const isMobile = window.innerWidth <= 768;
2037    if (isSpaceColonyTheme && !isMobile) {
2038      mhc.style.alignItems = 'flex-end';
2039      mhc.style.flexGrow = '0';
2040      mhc.style.width = '';
2041    } else if (isSpaceColonyTheme && isMobile) {
2042      mhc.style.alignItems = 'flex-start';
2043      mhc.style.justifyContent = 'flex-start';
2044      mhc.style.flexGrow = '';
2045      mhc.style.width = '100%';
2046    } else if (!isMobile && mode === 'stacked') {
2047      mhc.style.alignItems = 'flex-start';
2048      mhc.style.flexGrow = '';
2049      mhc.style.width = '';
2050    } else {
2051      mhc.style.alignItems = '';
2052      mhc.style.flexGrow = '';
2053      mhc.style.width = '';
2054    }
2055  }
2056  if (typeof window.syncContentMarginToHorizontalHeader === 'function') {
2057    window.syncContentMarginToHorizontalHeader();
2058  }
2059}
2060window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles;
2061
2062function parseHorizontalHeaderHeightPx(value, fallback) {
2063  if (value === null || value === undefined) {
2064    return fallback;
2065  }
2066  const parsed = parseInt(String(value).trim(), 10);
2067  if (Number.isNaN(parsed) || parsed < 1) {
2068    return fallback;
2069  }
2070  return parsed;
2071}
2072
2073function getUserHorizontalHeaderHeightPx() {
2074  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2075    const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0);
2076    if (fromSetting > 0) {
2077      return fromSetting;
2078    }
2079  }
2080  const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim();
2081  const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0);
2082  if (parsedVar > 0) {
2083    return parsedVar;
2084  }
2085  const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim();
2086  const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0);
2087  if (parsedTheme > 0) {
2088    return parsedTheme;
2089  }
2090  return 60;
2091}
2092
2093function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) {
2094  const headerVal = headerPx + 'px';
2095  const marginVal = contentMarginPx + 'px';
2096  const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim();
2097  const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim();
2098  if (currentHeader === headerVal && currentMargin === marginVal) {
2099    return;
2100  }
2101  document.documentElement.style.setProperty('--mobile-header-height', headerVal);
2102  document.documentElement.style.setProperty('--content-margin-top', marginVal);
2103}
2104
2105function measureHorizontalHeaderNaturalHeight(header) {
2106  const el = header;
2107  const prevHeight = el.style.getPropertyValue('height');
2108  const prevMinHeight = el.style.getPropertyValue('min-height');
2109  /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap
2110     doesn’t floor the measurement when the viewport widens back to one row. */
2111  el.style.setProperty('height', 'auto', 'important');
2112  el.style.setProperty('min-height', '0', 'important');
2113  const measured = Math.ceil(el.getBoundingClientRect().height);
2114  if (prevHeight) {
2115    el.style.setProperty('height', prevHeight);
2116  } else {
2117    el.style.removeProperty('height');
2118  }
2119  if (prevMinHeight) {
2120    el.style.setProperty('min-height', prevMinHeight);
2121  } else {
2122    el.style.removeProperty('min-height');
2123  }
2124  return measured;
2125}
2126
2127function applyHorizontalHeaderContentInset() {
2128  if (!document.body.classList.contains('menu-layout-horizontal')) {
2129    return;
2130  }
2131  if (window._horizontalHeaderInsetApplying) {
2132    return;
2133  }
2134  const header = document.querySelector('.mobile-header');
2135  if (!header) {
2136    return;
2137  }
2138
2139  window._horizontalHeaderInsetApplying = true;
2140  try {
2141    const userH = getUserHorizontalHeaderHeightPx();
2142    /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh;
2143       leaving that on --top-bar-height-setting makes calc(var(...) - 8px) → 0 and hides the logo. */
2144    document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px');
2145    const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open');
2146    const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked');
2147    const isPhone = window.innerWidth <= 768;
2148
2149    /*
2150     * Phone hamburger chrome: fixed Style Customizer height (link row is hidden).
2151     * Desktop inline may wrap beside the logo — measure like stacked so the bar can grow.
2152     * Stacked / open submenu: always measure.
2153     */
2154    if (!hasExpandedMenu && !isStackedLogo && isPhone) {
2155      setHorizontalHeaderHeightVars(userH, userH);
2156      return;
2157    }
2158
2159    /* Desktop inline (wrap-capable), stacked logo, or open submenu */
2160    const measured = measureHorizontalHeaderNaturalHeight(header);
2161    if (!measured || measured < 1) {
2162      setHorizontalHeaderHeightVars(userH, userH);
2163      return;
2164    }
2165
2166    let maxH = Math.max(200, Math.round(window.innerHeight * 0.5));
2167    const clamped = Math.min(Math.max(measured, userH), maxH);
2168    /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */
2169    const contentMargin = clamped > userH ? clamped + 8 : clamped;
2170    setHorizontalHeaderHeightVars(clamped, contentMargin);
2171  } finally {
2172    window._horizontalHeaderInsetApplying = false;
2173  }
2174}
2175
2176function ensureHorizontalHeaderHeightObserver() {
2177  if (typeof ResizeObserver === 'undefined') {
2178    return;
2179  }
2180  const header = document.querySelector('.mobile-header');
2181  if (!header) {
2182    return;
2183  }
2184  if (window._horizontalHeaderResizeObserver) {
2185    window._horizontalHeaderResizeObserver.disconnect();
2186  }
2187  window._horizontalHeaderResizeObserver = new ResizeObserver(function() {
2188    applyHorizontalHeaderContentInset();
2189  });
2190  window._horizontalHeaderResizeObserver.observe(header);
2191}
2192
2193function syncContentMarginToHorizontalHeader() {
2194  if (!document.body.classList.contains('menu-layout-horizontal')) {
2195    return;
2196  }
2197  requestAnimationFrame(function() {
2198    requestAnimationFrame(function() {
2199      applyHorizontalHeaderContentInset();
2200      ensureHorizontalHeaderHeightObserver();
2201    });
2202  });
2203}
2204window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader;
2205window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset;
2206
2207function getEffectiveMenuLayout() {
2208  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') {
2209    return window.MenuStyleCustomizer.getEffectiveMenuLayout();
2210  }
2211  if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal';
2212  if (document.body.classList.contains('menu-layout-top')) return 'top';
2213  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2214    return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2215  }
2216  return 'sidebar';
2217}
2218
2219function ensureCorrectLayoutApplied() {
2220  // Check if MenuStyleCustomizer and its necessary parts exist
2221  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2222    // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics
2223    const fromSettings = window.MenuStyleCustomizer.settings.menuLayout;
2224    const currentLayout = fromSettings || getEffectiveMenuLayout();
2225    console.log(`Ensuring layout is applied: ${currentLayout}`);
2226
2227    // Apply the layout styles (CSS classes, display properties)
2228    window.MenuStyleCustomizer._applyMenuLayout(currentLayout);
2229
2230    // --- Force Header Structure Rebuild for Horizontal Layout ---
2231    if (currentLayout === 'horizontal') {
2232      const mobileHeader = document.querySelector('.mobile-header');
2233      if (mobileHeader) {
2234        let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2235        // Ensure the main content container exists
2236        if (!mobileHeaderContent) {
2237          mobileHeaderContent = document.createElement('div');
2238          mobileHeaderContent.className = 'mobile-header-content';
2239          const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn');
2240          if (hamburgerBtn) {
2241            mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn);
2242          } else {
2243            mobileHeader.appendChild(mobileHeaderContent);
2244          }
2245        }
2246        // Ensure specific sub-containers exist, creating them if necessary
2247        // This guarantees the structure is present before renderHorizontalMenu fills them.
2248        if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) {
2249            const logoContainer = document.createElement('div');
2250            logoContainer.className = 'horizontal-header-logo';
2251            mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild);
2252        }
2253        if (!mobileHeaderContent.querySelector('.horizontal-menu
2253-container')) {
2254            const menuContainer = document.createElement('div');
2255            menuContainer.className = 'horizontal-menu-container';
2256            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2257            if (logoContainer) {
2258                logoContainer.insertAdjacentElement('afterend', menuContainer);
2259            } else { // Fallback if logo container wasn't found/created
2260                 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild);
2261            }
2262        }
2263        // Apply flexbox styling to position social container at far right
2264        mobileHeaderContent.style.display = 'flex';
2265        const isSpaceColonyTheme = isSpaceColonySiteTheme();
2266        const isMobile = window.innerWidth <= 768;
2267        mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2268        mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2269        if (isSpaceColonyTheme && !isMobile) {
2270          mobileHeaderContent.style.flexGrow = '0';
2271          mobileHeaderContent.style.width = '';
2272        } else {
2273          mobileHeaderContent.style.flexGrow = '';
2274          mobileHeaderContent.style.width = '100%';
2275        }
2276        
2277        // Create a flex container for logo and menu items
2278        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2279        if (!logoMenuContainer) {
2280            console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...');
2281            const logoMenuWrapper = document.createElement('div');
2282            logoMenuWrapper.className = 'logo-menu-container';
2283            // Move logo and menu into the wrapper
2284            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2285            const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
2286            
2287            console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer);
2288            console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer);
2289            
2290            if (logoContainer && menuContainer) {
2291                // Insert wrapper before the logo
2292                mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2293                // Move both containers into the wrapper
2294                logoMenuWrapper.appendChild(logoContainer);
2295                logoMenuWrapper.appendChild(menuContainer);
2296                console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper');
2297            } else {
2298                console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper');
2299            }
2300        } else {
2301            console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists');
2302        }
2303        if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2304          applyHorizontalLogoMenuLayoutStyles();
2305        }
2306        
2307        // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created)
2308        if (!mobileHeaderContent.querySelector('.horizontal-social-container')) {
2309            const socialContainer = document.createElement('div');
2310            socialContainer.className = 'horizontal-social-container';
2311            // Insert at the end (far right) of the mobile header content
2312            mobileHeaderContent.appendChild(socialContainer);
2313        }
2314      }
2315    }
2316     // --- End Header Structure Rebuild ---
2317
2318
2319    // Re-render the correct menu structure based on the applied layout
2320    if (currentLayout === 'horizontal') {
2321      // If horizontal layout, render the horizontal menu
2322      if (typeof renderHorizontalMenu === 'function') {
2323        renderHorizontalMenu();
2324      } else {
2325        console.warn("renderHorizontalMenu function not found.");
2326      }
2327      
2328      // After rendering horizontal menu, update mobile header content
2329      // This ensures mobile header shows logo instead of menu items on mobile
2330      setTimeout(() => {
2331        if (typeof updateMobileHeaderContent === 'function') {
2332          updateMobileHeaderContent();
2333        }
2334      }, 100);
2335    } else {
2336      // For sidebar or top layouts, render the standard gallery tree
2337      if (typeof renderGalleries === 'function') {
2338        renderGalleries();
2339      } else {
2340        console.warn("renderGalleries function not found.");
2341      }
2342      
2343      // Update mobile header content for other layouts too
2344      setTimeout(() => {
2345        if (typeof updateMobileHeaderContent === 'function') {
2346          updateMobileHeaderContent();
2347        }
2348      }, 100);
2349    }
2350  } else {
2351    // Log a warning if the necessary components aren't available
2352    console.warn("MenuStyleCustomizer or its methods not available to re-apply layout.");
2353  }
2354}
2355
2356function applyLayoutStylesOnly() {
2357    // Check if MenuStyleCustomizer and its necessary parts exist
2358    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2359        // Get the current layout setting, default to 'sidebar' if not set
2360        const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2361        console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`);
2362        // Apply the layout styles (CSS classes, display properties)
2363        window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles
2364    } else {
2365        console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available.");
2366    }
2367}
2368
2369// Add this function to ensure sidebar content for mobile
2370function ensureSidebarContentForMobile() {
2371    const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
2372    const isMobileView = window.innerWidth <= 768;
2373    
2374    if (isHorizontalLayout && isMobileView) {
2375        console.log('Ensuring sidebar content for mobile horizontal layout');
2376        
2377        // Make sure the sidebar tree is populated
2378        const galleryTreeContainer = document.getElementById('galleryTree');
2379        if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) {
2380            console.log('Sidebar tree is empty, populating with galleries');
2381            
2382            // Render the galleries in the sidebar tree for mobile navigation
2383            if (typeof renderGalleries === 'function') {
2384                renderGalleries();
2385            }
2386        }
2387        
2388        // Also ensure sidebar elements are rendered
2389        if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') {
2390            console.log('Re-rendering sidebar elements for mobile');
2391            window.SidebarManager.renderElements();
2392        }
2393    }
2394}
2395
2396function renderTextLogo(container, htmlContentFromSidebarElement) {
2397  if (!container) {
2398    console.error("[RenderTextLogo] Error: Logo container not found.");
2399    return;
2400  }
2401
2402  container.innerHTML = '';
2403
2404  const raw = (htmlContentFromSidebarElement || '').trim();
2405  if (!raw) {
2406    container.innerHTML = '<span class="site-title-text">Menu</span>';
2407  } else if (!/<[a-z][sS]*>/i.test(raw)) {
2408    // Plain text — wrap so theme CSS can target .site-title-text
2409    container.innerHTML = '<span class="site-title-text">' + raw + '</span>';
2410  } else {
2411    container.innerHTML = raw;
2412    const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
2413    if (titleEl && !titleEl.classList.contains('site-title-text')) {
2414      titleEl.classList.add('site-title-text');
2415    }
2416  }
2417
2418  // Do not force heading/paragraph margins to 0 — menu-styles.css matches
2419  // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter.
2420
2421  console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`);
2422}
2423
2424function renderHorizontalMenu() {
2425  console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined');
2426  
2427  // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts)
2428  const currentLayout = getEffectiveMenuLayout();
2429  
2430  const isHorizontalLayout = currentLayout === 'horizontal';
2431  console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout);
2432  
2433  // If not horizontal layout, only handle logo/alignment, then return early
2434  if (!isHorizontalLayout) {
2435    console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation");
2436    // Still handle logo/alignment for sidebar layout
2437    const mobileHeader = document.querySelector('.mobile-header');
2438    if (mobileHeader) {
2439      const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2440      if (logoContainer) {
2441        // Handle logo rendering only (code below will handle this)
2442        // But we'll return before creating menu items
2443      }
2444    }
2445    // Return early - don't create horizontal menu items in sidebar layout
2446    return;
2447  }
2448  
2449  // Wait for sidebar elements to be available before rendering
2450  if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) {
2451    console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting...");
2452    const waitForSidebarElements = (attempts = 0) => {
2453      const maxAttempts = 20; // Increased attempts for slower loads
2454      if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2455        console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render.");
2456        renderHorizontalMenu();
2457      } else if (attempts < maxAttempts) {
2458        const attemptNum = attempts + 1;
2459        setTimeout(() => waitForSidebarElements(attempts + 1), 100);
2460      } else {
2461        console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway.");
2462        // Continue with render even if sidebar elements aren't loaded
2463      }
2464    };
2465    waitForSidebarElements();
2466    return;
2467  }
2468  
2469  const mobileHeader = document.querySelector('.mobile-header');
2470  if (!mobileHeader) {
2471    console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND.");
2472    return;
2473  }
2474
2475  // Apply flexbox styling to position social container at far right
2476  const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2477  if (mobileHeaderContent) {
2478    mobileHeaderContent.style.display = 'flex';
2479    const isSpaceColonyTheme = isSpaceColonySiteTheme();
2480    const isMobile = window.innerWidth <= 768;
2481    mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2482    mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2483    if (isSpaceColonyTheme && !isMobile) {
2484      mobileHeaderContent.style.flexGrow = '0';
2485      mobileHeaderContent.style.width = '';
2486    } else {
2487      mobileHeaderContent.style.flexGrow = '';
2488      mobileHeaderContent.style.width = '100%';
2489    }
2490    
2491    // Create a flex container for logo and menu items
2492    const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2493    if (!logoMenuContainer) {
2494      console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...');
2495      const logoMenuWrapper = document.createElement('div');
2496      logoMenuWrapper.className = 'logo-menu-container';
2497      // Move logo and menu into the wrapper
2498      const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2499      const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2499-container');
2500      
2501      console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer);
2502      console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer);
2503      
2504      if (logoContainer && menuContainer) {
2505        // Insert wrapper before the logo
2506        mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2507        // Move both containers into the wrapper
2508        logoMenuWrapper.appendChild(logoContainer);
2509        logoMenuWrapper.appendChild(menuContainer);
2510        console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper');
2511      } else {
2512        console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper');
2513      }
2514    } else {
2515      console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists');
2516    }
2517    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2518      applyHorizontalLogoMenuLayoutStyles();
2519    }
2520  }
2521
2522  const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2523  if (!logoContainer) {
2524    console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header.");
2525    return;
2526  }
2527  
2528  console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container");
2529  logoContainer.innerHTML = ''; // Clear existing logo content first
2530
2531  // FIXED: Check if we're on mobile - if so, don't render the horizontal logo
2532  const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed
2533  
2534  if (isMobileView) {
2535    console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering");
2536    // On mobile, the logo will be handled by updateMobileHeaderContent()
2537  } else {
2538    console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo");
2539    
2540    // Get the logo element data
2541    let logoElementData = null;
2542    try {
2543      logoElementData = getFirstSidebarElementForHeader();
2544      console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData));
2545    } catch (error) {
2546      console.error('[RenderHorizontalMenu] Error getting sidebar element:', error);
2547      logoElementData = getFirstSidebarElementFallback();
2548    }
2549
2550     if (logoElementData) {
2551      if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) {
2552        console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl);
2553        
2554        const img = document.createElement('img');
2555        img.src = logoElementData.imageUrl || logoElementData.imageData;
2556        img.alt = resolveBrandLogoAlt(logoElementData);
2557        img.className = 'sidebar-image';
2558        
2559        // Apply comprehensive styling to ensure visibility
2560        img.style.display = 'block';
2561        
2562        // --- FIX ---
2563        // The line below was removed. It was applying a fixed max-height of 50px,
2564        // overriding your CSS. Removing it allows your stylesheet to take control.
2565        // img.style.maxHeight = '50px'; 
2566        
2567        img.style.objectFit = 'contain';
2568        img.style.minWidth = '1px';
2569        img.style.minHeight = '0';
2570
2571        let logoDisplayNode = img;
2572        if (logoElementData.colorizeWithSiteColors &&
2573            window.SidebarManager &&
2574            typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
2575          const buildData = Object.assign({}, logoElementData);
2576          if (!buildData.imageWidth) {
2577            buildData.imageWidth = 'auto';
2578          }
2579          logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
2580        } else {
2581          img.style.width = 'auto';
2582          img.style.visibility = 'visible';
2583          img.style.opacity = '1';
2584        }
2585
2586        // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS)
2587        if (window.SidebarManager &&
2588            typeof window.SidebarManager._wrapWithImagePadding === 'function') {
2589          logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData);
2590        }
2591
2592        // Enhanced event listeners for debugging
2593        img.onload = () => {
2594          console.log('[RenderHorizontalMenu] Image successfully LOADED');
2595        };
2596        
2597        img.onerror = (error) => {
2598          console.error('[RenderHorizontalMenu] Image FAILED to load:', error);
2599          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2600        };
2601
2602        try {
2603          // Handle linking (similar to sidebar-manager.js)
2604          // Only add links in view mode, not edit mode
2605          // Explicit edit toggle only — Parameters.isInEditor is also true for logged-in admins
2606          const isEditing = document.body.classList.contains('edit-mode-active') ||
2607                           (typeof window.isInEditMode === 'function' && window.isInEditMode());
2608          
2609          // Debug: Log link properties and element data
2610          console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData);
2611          console.log('[RenderHorizontalMenu] Image link properties:', {
2612            linkType: logoElementData.linkType,
2613            linkUrl: logoElementData.linkUrl,
2614            linkPageId: logoElementData.linkPageId,
2615            linkTarget: logoElementData.linkTarget,
2616            isEditing: isEditing,
2617            hasLinkType: !!logoElementData.linkType,
2618            linkTypeValue: logoElementData.linkType,
2619            linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none',
2620            notEditing: !isEditing,
2621            bodyHasEditClass: document.body.classList.contains('edit-mode-active'),
2622            isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A',
2623            ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A'
2624          });
2625          
2626          // Check for link - handle undefined, null, empty string, or 'none'
2627          const hasLink = logoElementData.linkType && 
2628                         logoElementData.linkType !== 'none' && 
2629                         logoElementData.linkType !== '' &&
2630                         !isEditing;
2631          
2632          console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType);
2633          
2634          if (hasLink) {
2635            const link = document.createElement('a');
2636            link.target = logoElementData.linkTarget || '_self';
2637            let finalLinkHref = '#';
2638            let isValidLink = false;
2639            
2640            if (logoElementData.linkType === 'external' && logoElementData.linkUrl) {
2641              finalLinkHref = logoElementData.linkUrl;
2642              if (link.target === '_blank') {
2643                link.rel = 'noopener noreferrer';
2644              }
2645              isValidLink = true;
2646            } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2647              const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2648              const linkPageIdNum = parseInt(logoElementData.linkPageId);
2649              console.log('[RenderHorizontalMenu] Looking for internal page:', {
2650                linkPageId: logoElementData.linkPageId,
2651                linkPageIdNum: linkPageIdNum,
2652                allGalleriesLength: allGalleries ? allGalleries.length : 0,
2653                galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : []
2654              });
2655              
2656              const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2657              
2658              console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null);
2659              
2660              if (targetPage) {
2661                // Handle preview mode URLs
2662                const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
2663                console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode);
2664                
2665                if (isPreviewMode) {
2666                  const pathParts = window.location.pathname.split('/').filter(Boolean);
2667                  const siteGuid = pathParts[0];
2668                  console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug);
2669                  
2670                  if (siteGuid && targetPage.slug) {
2671                    finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
2672                    isValidLink = true;
2673                    console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref);
2674                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2675                    finalLinkHref = targetPage.url;
2676                    isValidLink = true;
2677                    console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref);
2678                  }
2679                } else {
2680                  if (targetPage.slug) {
2681                    finalLinkHref = '/' + targetPage.slug;
2682                    isValidLink = true;
2683                    console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref);
2684                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2685                    finalLinkHref = targetPage.url;
2686                    isValidLink = true;
2687                    console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref);
2688                  }
2689                }
2690              } else {
2691                console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId);
2692              }
2693            }
2694            
2695            if (isValidLink) {
2696              link.href = finalLinkHref;
2697              // Ensure link is clickable
2698              link.style.pointerEvents = 'auto';
2699              link.style.cursor = 'pointer';
2700              link.style.textDecoration = 'none';
2701              link.style.display = 'flex';
2702              link.style.alignItems = 'center';
2703              link.style.height = 'auto';
2704              link.style.maxHeight = '100%';
2705              link.style.minHeight = '0';
2706              link.style.lineHeight = '0';
2707              console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied');
2708              
2709              // For internal links, add click handler to use site navigation
2710              if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2711                // Store linkPageId in a variable that won't be affected by closure
2712                const linkPageId = logoElementData.linkPageId;
2713                const linkHref = finalLinkHref;
2714                
2715                // Add click handler with both capture and bubble phases to ensure it fires
2716                const clickHandler = (e) => {
2717                  console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e);
2718                  e.preventDefault();
2719                  e.stopPropagation();
2720                  e.stopImmediatePropagation();
2721                  
2722                  const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2723                  const linkPageIdNum = parseInt(linkPageId);
2724                  const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2725                  
2726                  console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null);
2727                  console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery);
2728                  
2729                  if (targetPage) {
2730                    if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
2731                      console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id);
2732                      window.loadPage(targetPage.id);
2733                    } else if (typeof window.loadGallery === 'function') {
2734                      console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id);
2735                      window.loadGallery(targetPage.id);
2736                    } else {
2737                      // Fallback to standard navigation
2738                      console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref);
2739                      window.location.href = linkHref;
2740                    }
2741                  } else {
2742                    // Fallback to standard navigation if page not found
2743                    console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref);
2744                    window.location.href = linkHref;
2745                  }
2746                  
2747                  return false;
2748                };
2749                
2750                // Add handler in capture phase to catch event early
2751                link.addEventListener('click', clickHandler, true);
2752                // Also add in bubble phase as backup
2753                link.addEventListener('click', clickHandler, false);
2754                
2755                // Also add mousedown as backup in case click is being prevented
2756                link.addEventListener('mousedown', (e) => {
2757                  if (e.button === 0) { // Left click only
2758                    console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler');
2759                    clickHandler(e);
2760                  }
2761                });
2762                
2763                console.log('[RenderHorizontalMenu] Click handlers attached to link');
2764              }
2765              
2766              link.appendChild(logoDisplayNode);
2767              logoContainer.appendChild(link);
2768              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer);
2769              
2770              // Verify link is in DOM and clickable
2771              setTimeout(() => {
2772                const verifyLink = logoContainer.querySelector('a');
2773                if (verifyLink) {
2774                  console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', {
2775                    href: verifyLink.href,
2776                    pointerEvents: window.getComputedStyle(verifyLink).pointerEvents,
2777                    cursor: window.getComputedStyle(verifyLink).cursor,
2778                    zIndex: window.getComputedStyle(verifyLink).zIndex,
2779                    position: window.getComputedStyle(verifyLink).position
2780                  });
2781                } else {
2782                  console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!');
2783                }
2784              }, 100);
2785            } else {
2786              console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink);
2787              logoContainer.appendChild(logoDisplayNode);
2788              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
2789            }
2790          } else {
2791            logoContainer.appendChild(logoDisplayNode);
2792            console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
2793          }
2794        } catch (e) {
2795          console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e);
2796          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2797        }
2798        
2799      } else if (logoElementData.type === 'text' && logoElementData.textContent) {
2800        console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent);
2801        renderTextLogo(logoContainer, logoElementData.textContent);
2802        
2803      } else {
2804        console.log('[RenderHorizontalMenu] Using title as fallback logo');
2805        renderTextLogo(logoContainer, logoElementData.title || 'Menu');
2806      }
2807    } else {
2808      console.log("[RenderHorizontalMenu] No logoElementData available. Using default text.");
2809      renderTextLogo(logoContainer, 'Menu');
2810    }
2811  }
2812
2813  // Render menu items (only on desktop, not on mobile)
2814  const menuContainer = mobileHeader.querySelector('.horizontal-menu-container');
2815  if (!menuContainer) {
2816    console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header.");
2817  } else {
2818    menuContainer.innerHTML = '';
2819    
2820    // Apply alignment class from settings
2821    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2822      const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left';
2823      const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left';
2824      menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center');
2825      menuContainer.classList.add('menu-align-' + alignment);
2826      console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-' 
2826+ alignment);
2827    }
2828    
2829    // Only populate menu items on desktop, not on mobile
2830    if (!isMobileView) {
2831      let galleryTree = [];
2832      if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') {
2833        galleryTree = createGalleryTree(galleries);
2834      } else {
2835        console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined.");
2836      }
2837      
2838      galleryTree.forEach(item => {
2839        const menuItemElement = createHorizontalMenuItem(item);
2840        if (menuItemElement) {
2841          menuContainer.appendChild(menuItemElement);
2842        }
2843      });
2844      if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
2845    } else {
2846      console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population");
2847    }
2848  }
2849
2850  // Render social icons (only on desktop, not on mobile)
2851  let socialContainer = mobileHeader.querySelector('.horizontal-social-container');
2852  if (!socialContainer) {
2853    // Fallback: create the social container if it doesn't exist
2854    console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it...");
2855    const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2856    if (mobileHeaderContent) {
2857      socialContainer = document.createElement('div');
2858      socialContainer.className = 'horizontal-social-container';
2859      mobileHeaderContent.appendChild(socialContainer);
2860      console.log("[RenderHorizontalMenu] Created .horizontal-social-container");
2861    } else {
2862      console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container");
2863    }
2864  }
2865  
2866  if (socialContainer) {
2867    socialContainer.innerHTML = '';
2868    
2869    // Only populate social icons on desktop, not on mobile
2870    if (!isMobileView) {
2871      const socialElement = getFirstSocialElementForHeader();
2872      if (socialElement && socialElement.type === 'social') {
2873        console.log('[RenderHorizontalMenu] Found social element, rendering social icons');
2874        renderSocialIcons(socialContainer, socialElement);
2875      } else {
2876        console.log('[RenderHorizontalMenu] No social element found or element is not social type');
2877        // Container remains empty - no impact on layout
2878      }
2879    } else {
2880      console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population");
2881    }
2882  }
2883
2884
2885
2886  if (typeof updateActiveStatesHorizontal === 'function') {
2887    updateActiveStatesHorizontal();
2888  }
2889
2890  if (!isMobileView && mobileHeaderContent) {
2891    mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) {
2892      if (!el.closest('.horizontal-header-logo')) {
2893        el.remove();
2894      }
2895    });
2896  }
2897  
2898setTimeout(() => {
2899  ensureSidebarContentForMobile();
2900}, 100);
2901
2902  if (typeof syncContentMarginToHorizontalHeader === 'function') {
2903    setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200);
2904  }
2905
2906  console.log("[RenderHorizontalMenu] Function COMPLETED.");
2907}
2908
2909/**
2910 * Renders social icons in the horizontal header container.
2911 * @param {HTMLElement} container - The container to render social icons in
2912 * @param {object} socialElement - The social element data from sidebar
2913 */
2914function renderSocialIcons(container, socialElement) {
2915  console.log('[RenderSocialIcons] Rendering social icons:', socialElement);
2916  
2917  try {
2918    // Safety check: ensure we have a valid social element
2919    if (!socialElement || socialElement.type !== 'social') {
2920      console.log('[RenderSocialIcons] Invalid social element - skipping rendering');
2921      return;
2922    }
2923
2924    /* Placeholders without URLs still need menu color (CSS also sets this). */
2925    container.style.color = 'var(--menu-color, #333)';
2926    
2927    // Get icon size with proper fallbacks
2928    let iconSize = 30;
2929    if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) {
2930      iconSize = socialElement.socialIconSize;
2931    } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) {
2932      iconSize = parseInt(socialElement.socialIconSize);
2933    }
2934    
2935    // Render icons directly from element data
2936    if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) {
2937      socialElement.socialIcons.forEach(icon => {
2938        // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible)
2939        const iconWrapper = document.createElement('div');
2940        iconWrapper.className = 'social-icon-wrapper';
2941        iconWrapper.style.cssText = 'width: ' + iconSize + 'px !important; height: ' + iconSize + 'px !important; display: flex !important; align-items: center !important; justify-content: center !important; color: inherit !important;';
2942
2943        const applySvgSizing = (host) => {
2944          const svg = host.querySelector('svg');
2945          if (!svg) return;
2946          svg.style.width = '100%';
2947          svg.style.height = '100%';
2948          svg.setAttribute('width', '100%');
2949          svg.setAttribute('height', '100%');
2950          svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
2951          svg.setAttribute('aria-hidden', 'true');
2952          svg.setAttribute('focusable', 'false');
2953          svg.style.color = 'inherit';
2954          svg.style.fill = 'currentColor';
2955          svg.style.stroke = 'currentColor';
2956        };
2957
2958        const hasUrl = !!(icon.url && String(icon.url).trim());
2959
2960        // Add link if URL exists; otherwise still render the glyph (placeholder icons)
2961        if (hasUrl) {
2962          const link = document.createElement('a');
2963          link.href = icon.url;
2964          link.target = '_blank';
2965          link.rel = 'noopener noreferrer';
2966          link.classList.add('horizontal-social-link');
2967          link.style.cssText = 'display: flex !important; align-items: center !important; justify-content: center !important; width: 100% !important; height: 100% !important; color: inherit !important; text-decoration: none !important;';
2968          const socialLabel = (icon.alt && String(icon.alt).trim())
2969            || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function'
2970              ? window.SidebarManager._socialIconTypeLabel(icon.type)
2971              : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : ''))
2972            || 'Social link';
2973          const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1);
2974          link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)');
2975          
2976          if (icon.type === 'custom' && icon.iconUrl) {
2977            // Render uploaded image
2978            const img = document.createElement('img');
2979            img.src = icon.iconUrl;
2980            img.alt = '';
2981            img.setAttribute('aria-hidden', 'true');
2982            img.style.width = '100%';
2983            img.style.height = '100%';
2984            img.style.objectFit = 'contain';
2985            link.appendChild(img);
2986          } else {
2987            // Add SVG - use SidebarManager's method if available, otherwise create basic SVG
2988            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
2989              link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
2990            } else {
2991              // Fallback: create a simple SVG placeholder
2992              link.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>';
2993            }
2994            applySvgSizing(link);
2995          }
2996          iconWrapper.appendChild(link);
2997        } else {
2998          // Placeholder: no URL — still paint with menu color via container inheritance
2999          iconWrapper.setAttribute('aria-hidden', 'true');
3000          if (icon.type === 'custom' && icon.iconUrl) {
3001            const img = document.createElement('img');
3002            img.src = icon.iconUrl;
3003            img.alt = '';
3004            img.style.width = '100%';
3005            img.style.height = '100%';
3006            img.style.objectFit = 'contain';
3007            iconWrapper.appendChild(img);
3008          } else {
3009            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3010              iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3011            } else {
3012              iconWrapper.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>';
3013            }
3014            applySvgSizing(iconWrapper);
3015          }
3016        }
3017        
3018        container.appendChild(iconWrapper);
3019        console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url);
3020      });
3021    } else {
3022      console.log('[RenderSocialIcons] No social icons found in element');
3023    }
3024  } catch (error) {
3025    console.error('[RenderSocialIcons] Error rendering social icons:', error);
3026  }
3027}
3028
3029/**
3030 * Gets the appropriate icon text or class for a social platform.
3031 * @param {string} platform - The social platform name
3032 * @returns {string} The icon text or class
3033 */
3034function getSocialIconText(platform) {
3035  const iconMap = {
3036    'facebook': 'f',
3037    'twitter': 't',
3038    'instagram': 'i',
3039    'linkedin': 'in',
3040    'youtube': 'yt',
3041    'pinterest': 'p',
3042    'tiktok': 'tt',
3043    'snapchat': 'sc',
3044    'whatsapp': 'wa',
3045    'telegram': 'tg',
3046    'discord': 'd',
3047    'github': 'gh',
3048    'email': '@',
3049    'phone': '📞',
3050    'website': '🌐'
3051  };
3052  
3053  return iconMap[platform.toLowerCase()] || '•';
3054}
3055
3056
3057
3058
3059/**
3060 * Creates a single top-level horizontal menu item and its submenu if it has children.
3061 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal)
3062 * @param {object} galleryItem - The gallery item data.
3063 * @returns {HTMLElement | null} The created menu item element, or null if not rendered.
3064 */
3065function createHorizontalMenuItem(galleryItem) {
3066  if (galleryItem.visible === false || galleryItem.isSpacer) {
3067      return null;
3068  }
3069
3070  const menuItem = document.createElement('div');
3071  menuItem.className = 'horizontal-menu-item';
3072  menuItem.dataset.id = String(galleryItem.id);
3073  menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3074
3075  const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length);
3076  const isNavigableParent = isNavigableMenuParent(galleryItem);
3077  let titleEl;
3078  if (hasVisibleChildren && !isNavigableParent) {
3079    titleEl = document.createElement('span');
3080    titleEl.className = 'horizontal-menu-label';
3081    titleEl.textContent = galleryItem.title;
3082    menuItem.setAttribute('aria-haspopup', 'true');
3083    menuItem.setAttribute('aria-expanded', 'false');
3084  } else {
3085    titleEl = document.createElement('a');
3086    titleEl.className = 'horizontal-menu-label';
3087    titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3088    titleEl.textContent = galleryItem.title;
3089    if (galleryItem.isExternal) {
3090      titleEl.target = '_blank';
3091      titleEl.rel = 'noopener noreferrer';
3092    } else {
3093      titleEl.addEventListener('click', (e) => { e.preventDefault(); });
3094    }
3095  }
3096  menuItem.appendChild(titleEl);
3097
3098  // Ensure activeGalleryId is treated as a number for comparison during initial render
3099  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3100  if (galleryItem.id === currentGlobalActiveId) {
3101      menuItem.classList.add('active');
3102  }
3103
3104  let leaveTimer; // For mouseleave timeout to close dropdown
3105
3106  if (galleryItem.children && galleryItem.children.length > 0) {
3107      const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer);
3108
3109      if (visibleChildren.length > 0) {
3110          menuItem.classList.add('has-children');
3111          if (isNavigableParent) {
3112              menuItem.setAttribute('aria-haspopup', 'true');
3113              menuItem.setAttribute('aria-expanded', 'false');
3114          }
3115
3116          const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span');
3117          toggleIconContainer.className = 'submenu-toggle-horizontal';
3118          if (isNavigableParent) {
3119              toggleIconContainer.type = 'button';
3120              toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3121              toggleIconContainer.setAttribute('aria-expanded', 'false');
3122              toggleIconContainer.addEventListener('click', function(event) {
3123                  event.preventDefault();
3124                  event.stopPropagation();
3125                  const willExpand = !menuItem.classList.contains('expanded');
3126                  menuItem.classList.toggle('expanded', willExpand);
3127                  menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3128                  toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3129                  const icon = toggleIconContainer.querySelector('.icon.toggle-icon');
3130                  if (icon) icon.classList.toggle('rotated', willExpand);
3131              });
3132          }
3133          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3134          svgIcon.setAttribute("class", "icon toggle-icon");
3135          svgIcon.setAttribute("viewBox", "0 0 24 24");
3136          svgIcon.setAttribute("fill", "none");
3137          svgIcon.setAttribute("stroke", "currentColor");
3138          svgIcon.setAttribute("stroke-width", "2");
3139          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3140          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3141          svgIcon.appendChild(polyline);
3142          toggleIconContainer.appendChild(svgIcon);
3143          menuItem.appendChild(toggleIconContainer);
3144
3145          const subMenu = document.createElement('div');
3146          subMenu.className = 'horizontal-dropdown';
3147          const subMenuList = document.createElement('ul');
3148          subMenuList.className = 'horizontal-dropdown-list';
3149
3150          visibleChildren.forEach((childItem) => {
3151              const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call
3152              if (subMenuItemElement) {
3153                  subMenuList.appendChild(subMenuItemElement);
3154              }
3155          });
3156
3157          if (subMenuList.hasChildNodes()) {
3158              subMenu.appendChild(subMenuList);
3159              menuItem.appendChild(subMenu);
3160
3161              menuItem.addEventListener('mouseenter', function() {
3162                  clearTimeout(leaveTimer);
3163                  document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3164                      if (openItem !== this) {
3165                          openItem.classList.remove('expanded');
3166                          openItem.setAttribute('aria-expanded', 'false');
3167                          const otherIcon = openItem.querySelector('.icon.toggle-icon');
3168                          if (otherIcon) otherIcon.classList.remove('rotated');
3169                      }
3170                  });
3171                  this.classList.add('expanded');
3172                  this.setAttribute('aria-expanded', 'true');
3173                  if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true');
3174                  const currentIcon = this.querySelector('.icon.toggle-icon');
3175                  if (currentIcon) currentIcon.classList.add('rotated');
3176              });
3177
3178              menuItem.addEventListener('mouseleave', function(event) {
3179                  // Check if we're moving to the dropdown or a flyout
3180                  const relatedTarget = event.relatedTarget;
3181                  const isMovingToDropdown = relatedTarget && (
3182                      relatedTarget.closest('.horizontal-dropdown') ||
3183                      relatedTarget.classList.contains('horizontal-submenu-item') ||
3184                      relatedTarget.closest('.horizontal-submenu-item')
3185                  );
3186                  const isMovingToFlyout = relatedTarget && (
3187                      relatedTarget.classList.contains('horizontal-nested-dropdown') ||
3188                      relatedTarget.closest('.horizontal-nested-dropdown')
3189                  );
3190                  
3191                  if (!isMovingToDropdown && !isMovingToFlyout) {
3192                      console.log('Main menu item mouseleave - starting timer');
3193                      leaveTimer = setTimeout(() => {
3194                          // Check if there's an active flyout - if so, keep parent open
3195                          // Use the same multiple detection methods as the submenu handler
3196                          const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3197                                             document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3198                                             document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3199                          console.log('Main menu timer fired - active flyout:', activeFlyout);
3200                          if (!activeFlyout) {
3201                              console.log('Main menu closing dropdown - no active flyout');
3202                              this.classList.remove('expanded');
3203                              this.setAttribute('aria-expanded', 'false');
3204                              if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false');
3205                              const currentIcon = this.querySelector('.icon.toggle-icon');
3206                              if (currentIcon) currentIcon.classList.remove('rotated');
3207                          } else {
3208                              console.log('Main menu keeping dropdown open - flyout is active');
3209                          }
3210                      }, 200);
3211                  } else {
3212                      console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)');
3213                  }
3214              });
3215
3216              subMenu.addEventListener('mouseenter', function() {
3217                  const parentMenuItem = this.closest('.horizontal-menu
3217-item');
3218                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3219                      clearTimeout(parentMenuItem.leaveTimer);
3220                      parentMenuItem.leaveTimer = null;
3221                  }
3222              });
3223
3224              subMenu.addEventListener('mouseleave', function(event) {
3225                  const parentMenuItem = this.closest('.horizontal-menu-item');
3226                  if (parentMenuItem) {
3227                      // Check if we're moving to another element within the same dropdown
3228                      const relatedTarget = event.relatedTarget;
3229                      console.log('SubMenu mouseleave - relatedTarget:', relatedTarget);
3230                      console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null');
3231                      
3232                      // Check if we're moving to another element within the same dropdown
3233                      const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown');
3234                      console.log('Current dropdown found:', !!currentDropdown);
3235                      console.log('Current dropdown element:', currentDropdown);
3236                      console.log('RelatedTarget:', relatedTarget);
3237                      console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null');
3238                      
3239                      // Check if relatedTarget is within the same dropdown container
3240                      const isMovingWithinDropdown = relatedTarget && currentDropdown && (
3241                          relatedTarget.closest('.horizontal-dropdown') === currentDropdown ||
3242                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3243                          relatedTarget.closest('.horizontal-submenu-item') ||
3244                          currentDropdown.contains(relatedTarget)
3245                      );
3246                      
3247                      // Alternative approach: check if relatedTarget is within the same parent menu item
3248                      const parentMenuContainer = this.closest('.horizontal-menu-item');
3249                      const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget);
3250                      console.log('Is moving within parent menu item:', isMovingWithinParent);
3251                      console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown');
3252                      console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null');
3253                      
3254                      // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside)
3255                      // Get the parent menu item's dropdown container
3256                      const parentMenuItem = this.closest('.horizontal-menu-item');
3257                      console.log('Parent menu item found:', !!parentMenuItem);
3258                      const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null;
3259                      console.log('Dropdown container found:', !!dropdownContainer);
3260                      if (dropdownContainer) {
3261                          console.log('Dropdown container classes:', dropdownContainer.className);
3262                          console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display);
3263                          console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position);
3264                      }
3265                      
3266                      // Get bounds from the visible dropdown (not the hidden one)
3267                      let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 };
3268                      if (dropdownContainer) {
3269                          const computedStyle = window.getComputedStyle(dropdownContainer);
3270                          if (computedStyle.display !== 'none') {
3271                              dropdownRect = dropdownContainer.getBoundingClientRect();
3272                          } else {
3273                              // If dropdown is hidden, try to get bounds from the parent menu item
3274                              const parentRect = parentMenuItem.getBoundingClientRect();
3275                              dropdownRect = parentRect;
3276                          }
3277                      }
3278                      const mouseX = event.clientX;
3279                      const mouseY = event.clientY;
3280                      const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 
3281                                                     mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom;
3282                      
3283                      console.log('Dropdown bounds:', {
3284                          left: dropdownRect.left,
3285                          right: dropdownRect.right,
3286                          top: dropdownRect.top,
3287                          bottom: dropdownRect.bottom,
3288                          mouseX,
3289                          mouseY,
3290                          mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom
3291                      });
3292                      
3293                      // If there's an active flyout, always keep the parent dropdown open
3294                      // Check multiple ways to detect active flyouts
3295                      const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3296                                         document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3297                                         document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3298                      console.log('Active flyout found:', !!activeFlyout);
3299                      if (activeFlyout) {
3300                          console.log('Active flyout display style:', activeFlyout.style.display);
3301                          console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display);
3302                          console.log('Active flyout visibility style:', activeFlyout.style.visibility);
3303                      }
3304                      const hasActiveFlyout = !!activeFlyout;
3305                      
3306                      // If the dropdown is hidden (display: none), we can't get accurate bounds
3307                      // So let's use a simpler approach - if we're moving to another submenu item, keep open
3308                      const isMovingToSubmenuItem = relatedTarget && (
3309                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3310                          relatedTarget.closest('.horizontal-submenu-item')
3311                      );
3312                      console.log('Is moving to submenu item:', isMovingToSubmenuItem);
3313                      console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null');
3314                      console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null');
3315                      
3316                      // If we're moving to another submenu item within the same dropdown, al
3316ways keep open
3317                      // OR if there's an active flyout (regardless of where the mouse is), keep open
3318                      // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open
3319                      const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem;
3320                      
3321                      console.log('Is moving within dropdown:', isMovingWithinDropdown);
3322                      console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds);
3323                      console.log('Has active flyout:', hasActiveFlyout);
3324                      console.log('Should keep open:', shouldKeepOpen);
3325                      
3326                      if (!shouldKeepOpen) {
3327                          console.log('SubMenu mouseleave - starting timer (moving outside dropdown)');
3328                          parentMenuItem.leaveTimer = setTimeout(() => {
3329                              // Check if there's an active flyout - if so, keep parent open
3330                              const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3331                              console.log('Timer fired - active flyout:', activeFlyout);
3332                              if (!activeFlyout) {
3333                                  console.log('Closing parent dropdown - no active flyout');
3334                                  parentMenuItem.classList.remove('expanded');
3335                                  const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3336                                  if (currentIcon) currentIcon.classList.remove('rotated');
3337                              } else {
3338                                  console.log('Keeping parent dropdown open - flyout is active');
3339                              }
3340                          }, 200);
3341                      } else {
3342                          console.log('SubMenu mouseleave - not starting timer (moving within dropdown)');
3343                      }
3344                  }
3345              });
3346          } else {
3347              menuItem.classList.remove('has-children');
3348              if (toggleIconContainer.parentNode) {
3349                  toggleIconContainer.remove();
3350              }
3351          }
3352      }
3353  }
3354
3355  // Click listener for items that are direct navigation links (not folders with children)
3356  if (!menuItem.classList.contains('has-children') || isNavigableParent) {
3357      menuItem.addEventListener('click', async (event) => {
3358          if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) {
3359              return;
3360          }
3361          event.stopPropagation();
3362          const clickedItemId = galleryItem.id; // Capture the ID of the clicked item
3363
3364          // Check if it's an external URL first
3365          if (galleryItem.isExternal && galleryItem.url) {
3366              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3367              return;
3368          }
3369
3370          if (isPageMenuItem(galleryItem)) {
3371              if (typeof loadPage === 'function') {
3372                  loadPage(clickedItemId, event); // loadPage is synchronous
3373                  if (typeof updateActiveStatesHorizontal === 'function') {
3374                      console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3375                      // Use a minimal delay even for sync operations if page rendering has its own async microtasks
3376                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3377                  }
3378              }
3379          } else { // It's a gallery
3380              if (typeof loadGallery === 'function') {
3381                  console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3382                  await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery
3383                  console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3384                  if (typeof updateActiveStatesHorizontal === 'function') {
3385                      setTimeout(() => {
3386                          console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3387                          updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId
3388                      }, 100); // Delay to allow gallery system to settle
3389                  }
3390              }
3391          }
3392
3393          // Close any other open dropdowns and flyouts
3394          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3395              // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click)
3396              if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) {
3397                 openItem.classList.remove('expanded');
3398                 openItem.setAttribute('aria-expanded', 'false');
3399                 const icon = openItem.querySelector('.icon.toggle-icon');
3400                 if (icon) icon.classList.remove('rotated');
3401              }
3402          });
3403          
3404          // Close all flyouts
3405          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3406              flyout.style.display = 'none';
3407              flyout.style.opacity = '0';
3408              flyout.style.visibility = 'hidden';
3409          });
3410          
3411          // Remove expanded class from all submenu items
3412          document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => {
3413              subItem.classList.remove('expanded');
3414          });
3415      });
3416  } else {
3417      // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior
3418      menuItem.setAttribute('tabindex', '0');
3419      menuItem.addEventListener('keydown', function(event) {
3420          if (event.key !== 'Enter' && event.key !== ' ') return;
3421          event.preventDefault();
3422          const isOpen = this.classList.contains('expanded');
3423          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3424              if (openItem !== this) {
3425                  openItem.classList.remove('expanded');
3426                  openItem.setAttribute('aria-expanded', 'false');
3427                  const otherIcon = openItem.querySelector('.icon.toggle-icon');
3428                  if (otherIcon) otherIcon.classList.remove('rotated');
3429              }
3430          });
3431          this.classList.toggle('expanded', !isOpen);
3432          this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false');
3433          const currentIcon = this.querySelector('.icon.toggle-icon');
3434          if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen);
3435      });
3436  }
3437  return menuItem;
3438}
3439
3440/**
3441* Creates a single horizontal submenu item.
3442* (UPDATED to handle nested children and async loadGallery)
3443* @param {object} galleryItem - The gallery item data for the submenu.
3444* @returns {HTMLElement | null} The created submenu item element, or null if not rendered.
3445*/
3446function createHorizontalSubMenuItem(galleryItem) {
3447  if (galleryItem.visible === false || galleryItem.isSpacer) {
3448      return null;
3449  }
3450
3451  const subMenuItem = document.createElement('li');
3452  subMenuItem.className = 'horizontal-submenu-item';
3453  subMenuItem.dataset.id = String(galleryItem.id);
3454  subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3455
3456  const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer);
3457  const isNavigableParent = isNavigableMenuParent(galleryItem);
3458  let titleSpan;
3459  if (subVisibleChildren.length > 0 && !isNavigableParent) {
3460      titleSpan = document.createElement('span');
3461      titleSpan.className = 'horizontal-menu-label';
3462      titleSpan.textContent = galleryItem.title;
3463  } else {
3464      titleSpan = document.createElement('a');
3465      titleSpan.className = 'horizontal-menu-label';
3466      titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3467      titleSpan.textContent = galleryItem.title;
3468      if (galleryItem.isExternal) {
3469          titleSpan.target = '_blank';
3470          titleSpan.rel = 'noopener noreferrer';
3471      } else {
3472          titleSpan.addEventListener('click', (e) => { e.preventDefault(); });
3473      }
3474  }
3475  subMenuItem.appendChild(titleSpan);
3476
3477  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3478  if (galleryItem.id === currentGlobalActiveId) {
3479      subMenuItem.classList.add('active');
3480      if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page');
3481  }
3482
3483  // Check if this submenu item has children (nested sub-sub items)
3484  if (galleryItem.children && galleryItem.children.length > 0) {
3485      const visibleChildren = subVisibleChildren;
3486      
3487      if (visibleChildren.length > 0) {
3488          subMenuItem.classList.add('has-children');
3489          
3490          // Add arrow indicator for nested items (same as top-level menu)
3491          const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span');
3492          arrowSpan.className = 'submenu-arrow';
3493          if (isNavigableParent) {
3494              arrowSpan.type = 'button';
3495              arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3496              arrowSpan.setAttribute('aria-expanded', 'false');
3497              arrowSpan.addEventListener('click', function(event) {
3498                  event.preventDefault();
3499                  event.stopPropagation();
3500                  const isOpen = subMenuItem.classList.contains('expanded');
3501                  if (isOpen) {
3502                      closeHorizontalNestedFlyout(subMenuItem);
3503                  } else {
3504                      openHorizontalNestedFlyout(subMenuItem);
3505                  }
3506              });
3507          }
3508          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3509          svgIcon.setAttribute("class", "icon toggle-icon");
3510          svgIcon.setAttribute("viewBox", "0 0 24 24");
3511          svgIcon.setAttribute("fill", "none");
3512          svgIcon.setAttribute("stroke", "currentColor");
3513          svgIcon.setAttribute("stroke-width", "2");
3514          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3515          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3516          svgIcon.appendChild(polyline);
3517          arrowSpan.appendChild(svgIcon);
3518          subMenuItem.appendChild(arrowSpan);
3519          
3520          // Create nested submenu as a true flyout
3521          const nestedSubMenu = document.createElement('div');
3522          nestedSubMenu.className = 'horizontal-nested-dropdown';
3523          
3524          // Create a list container for the nested items
3525          const nestedList = document.createElement('ul');
3526          nestedList.className = 'horizontal-nested-list';
3527          
3528          visibleChildren.forEach((childItem) => {
3529              const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call
3530              if (nestedSubMenuItem) {
3531                  nestedList.appendChild(nestedSubMenuItem);
3532              }
3533          });
3534          
3535          nestedSubMenu.appendChild(nestedList);
3536          
3537          if (nestedSubMenu.hasChildNodes()) {
3538              // Append to document body for true flyout behavior
3539              document.body.appendChild(nestedSubMenu);
3540              // Store reference to the flyout for cleanup
3541              const flyoutId = 'flyout-' + Date.now() + '-' + Math.random();
3542              subMenuItem.setAttribute('data-flyout-id', flyoutId);
3543              nestedSubMenu.setAttribute('data-flyout-id', flyoutId);
3544              
3545              console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug
3546              console.log('Flyout element:', nestedSubMenu); // Debug
3547              
3548              // Add hover events for nested submenu
3549              let nestedLeaveTimer;
3550
3551              // Shared open/close used by hover AND keyboard — mouse path unchanged
3552              function openHorizontalNestedFlyout(hostItem) {
3553                  if (!hostItem) return;
3554                  if (hostItem.nestedLeaveTimer) {
3555                      clearTimeout(hostItem.nestedLeaveTimer);
3556                  }
3557                  const parentMenuItem = hostItem.closest('.horizontal-menu-item');
3558                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3559                      clearTimeout(parentMenuItem.leaveTimer);
3560                  }
3561                  document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => {
3562                      if (openItem !== hostItem) {
3563                          openItem.classList.remove('expanded');
3564                          openItem.setAttribute('aria-expanded', 'false');
3565                          const otherFlyoutId = openItem.getAttribute('data-flyout-id');
3566                          if (otherFlyoutId) {
3567                              const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]');
3568                              if (otherFlyout) {
3569                                  otherFlyout.style.display = 'none';
3570                                  otherFlyout.style.opacity = '0';
3571                                  otherFlyout.style.visibility = 'hidden';
3572                              }
3573                          }
3574                      }
3575                  });
3576                  hostItem.classList.add('expanded');
3577                  hostItem.setAttribute('aria-expanded', 'true');
3578                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3579                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true');
3580                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3581                  if (!hostFlyoutId) return;
3582                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3583                  if (!nestedDropdown) return;
3584                  const rect = hostItem.getBoundingClientRect();
3585                  const parentDropdown = hostItem.closest('.horizontal-dropdown');
3586                  const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect;
3587                  const leftPos = parentRect.right - 1;
3588                  const topPos = rect.top;
3589                  nestedDropdown.style.left = leftPos + 'px';
3590                  nestedDropdown.style.top = topPos + 'px';
3591                  nestedDropdown.style.display = 'block';
3592                  nestedDropdown.style.opacity = '1';
3593                  nestedDropdown.style.visibility = 'visible';
3594                  nestedDropdown.style.position = 'fixed';
3595                  nestedDropdown.style.zIndex = '1251';
3596                  setTimeout(() => {
3597                      const dropdownRect = nestedDropdown.getBoundingClientRect();
3598                      if (dropdownRect.right > window.innerWidth) {
3599                          nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px';
3600                      }
3601                  }, 10);
3602              }
3603
3604              function closeHorizontalNestedFlyout(hostItem) {
3605                  if (!hostItem) return;
3606                  hostItem.classList.remove('expanded');
3607                  hostItem.setAttribute('aria-expanded', 'false');
3608                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3609                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false');
3610                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3611                  if (!hostFlyoutId) return;
3612                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3613                  if (nestedDropdown) {
3614                      nestedDropdown.style.display = 'none';
3615                      nestedDropdown.style.opacity = '0';
3616                      nestedDropdown.style.visibility = 'hidden';
3617                  }
3618              }
3619
3620              // Keyboard opens the same flyout hover shows — attrs on label so mouse click focus stays natural
3621              if (titleSpan && !isNavigableParent) {
3622                  titleSpan.setAttribute('tabindex', '0');
3623                  titleSpan.setAttribute('role', 'button');
3624                  titleSpan.setAttribute('aria-haspopup', 'true');
3625                  titleSpan.setAttribute('aria-expanded', 'false');
3626                  titleSpan.addEventListener('keydown', function(event) {
3627                      if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') {
3628                          event.preventDefault();
3629                          event.stopPropagation();
3630                          openHorizontalNestedFlyout(subMenuItem);
3631                          titleSpan.setAttribute('aria-expanded', 'true');
3632                          const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id');
3633                          const nestedDropdown = hostFlyoutId
3634                              ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]')
3635                              : null;
3636                          const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3636-label');
3637                          if (firstLink && typeof firstLink.focus === 'function') {
3638                              try { firstLink.focus(); } catch (e) {}
3639                          }
3640                      } else if (event.key === 'Escape' || event.key === 'ArrowLeft') {
3641                          event.preventDefault();
3642                          event.stopPropagation();
3643                          closeHorizontalNestedFlyout(subMenuItem);
3644                          titleSpan.setAttribute('aria-expanded', 'false');
3645                      }
3646                  });
3647              }
3648              
3649              subMenuItem.addEventListener('mouseenter', function() {
3650                  openHorizontalNestedFlyout(this);
3651                  if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true');
3652              });
3653              
3654              subMenuItem.addEventListener('mouseleave', function(event) {
3655                  // Check if we're moving to the flyout
3656                  const flyoutId = this.getAttribute('data-flyout-id');
3657                  const relatedTarget = event.relatedTarget;
3658                  const isMovingToFlyout = relatedTarget && flyoutId && 
3659                      relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]');
3660                  
3661                  // Also check if we're moving within the dropdown
3662                  const parentMenuItem = this.closest('.horizontal-menu-item');
3663                  const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget);
3664                  
3665                  console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout);
3666                  console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown);
3667                  
3668                  if (!isMovingToFlyout && !isMovingWithinDropdown) {
3669                      this.nestedLeaveTimer = setTimeout(() => {
3670                          closeHorizontalNestedFlyout(this);
3671                          if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false');
3672                          
3673                          // Now that the flyout is closed, allow the parent dropdown to close
3674                          const parentMenuItem = this.closest('.horizontal-menu-item');
3675                          if (parentMenuItem) {
3676                              setTimeout(() => {
3677                                  const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3678                                  if (!activeFlyout) {
3679                                      parentMenuItem.classList.remove('expanded');
3680                                      const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3681                                      if (currentIcon) currentIcon.classList.remove('rotated');
3682                                  }
3683                              }, 50);
3684                          }
3685                      }, 200);
3686                  } else {
3687                      console.log('Submenu item mouseleave - not starting timer (moving to flyout)');
3688                  }
3689              });
3690              
3691              nestedSubMenu.addEventListener('mouseenter', function() {
3692                  console.log('Flyout mouseenter - clearing timers');
3693                  clearTimeout(nestedLeaveTimer);
3694                  
3695                  // Clear the parent dropdown's leave timer to keep it open
3696                  const flyoutId = this.getAttribute('data-flyout-id');
3697                  if (flyoutId) {
3698                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3699                      if (parentSubItem) {
3700                          const parentMenuItem = parentSubItem.closest('.horizontal-menu-item');
3701                          if (parentMenuItem && parentMenuItem.leaveTimer) {
3702                              console.log('Clearing parent menu item leave timer');
3703                              clearTimeout(parentMenuItem.leaveTimer);
3704                              parentMenuItem.leaveTimer = null;
3705                          } else {
3706                              console.log('No parent menu item leave timer to clear');
3707                          }
3708                          if (parentSubItem.nestedLeaveTimer) {
3709                              console.log('Clearing submenu item nested leave timer');
3710                              clearTimeout(parentSubItem.nestedLeaveTimer);
3711                              parentSubItem.nestedLeaveTimer = null;
3712                          }
3713                      }
3714                  }
3715              });
3716              
3717              nestedSubMenu.addEventListener('mouseleave', function() {
3718                  const flyoutId = this.getAttribute('data-flyout-id');
3719                  if (flyoutId) {
3720                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3721                      if (parentSubItem) {
3722                          nestedLeaveTimer = setTimeout(() => {
3723                              closeHorizontalNestedFlyout(parentSubItem);
3724                              const label = parentSubItem.querySelector('.horizontal-menu
3724-label');
3725                              if (label) label.setAttribute('aria-expanded', 'false');
3726                          }, 200);
3727                      }
3728                  }
3729              });
3730          }
3731      }
3732  }
3733
3734  // Click listener for navigation (only if no children or if it's a direct link)
3735  if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) {
3736      subMenuItem.addEventListener('click', async (event) => {
3737          if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) {
3738              return;
3739          }
3740          event.stopPropagation();
3741          const clickedItemId = galleryItem.id;
3742
3743          // Check if it's an external URL first
3744          if (galleryItem.isExternal && galleryItem.url) {
3745              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3746              return;
3747          }
3748
3749          if (isPageMenuItem(galleryItem)) {
3750              if (typeof loadPage === 'function') {
3751                  loadPage(clickedItemId, event);
3752                  if (typeof updateActiveStatesHorizontal === 'function') {
3753                      console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3754                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3755                  }
3756              }
3757          } else { // It's a gallery
3758              if (typeof loadGallery === 'function') {
3759                  console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3760                  await loadGallery(clickedItemId, event);
3761                  console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3762                  if (typeof updateActiveStatesHorizontal === 'function') {
3763                      setTimeout(() => {
3764                          console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3765                          updateActiveStatesHorizontal(clickedItemId);
3766                      }, 100);
3767                  }
3768              }
3769          }
3770
3771          // Close all parent dropdowns and flyouts after navigation
3772          document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => {
3773              openItem.classList.remove('expanded');
3774              const icon = openItem.querySelector('.icon.toggle-icon');
3775              if (icon) icon.classList.remove('rotated');
3776          });
3777          
3778          // Close all flyouts
3779          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3780              flyout.style.display = 'none';
3781              flyout.style.opacity = '0';
3782              flyout.style.visibility = 'hidden';
3783          });
3784      });
3785  }
3786  
3787  return subMenuItem;
3788}
3789
3790/**
3791* Updates the 'active' class for items in the horizontal menu.
3792*/
3793
3794function updateActiveStatesHorizontal(forcedActiveId = null) {
3795  // Prioritize forcedActiveId if provided, otherwise use global state.
3796  // Ensure the ID is treated as a number for comparison.
3797  const targetId = forcedActiveId !== null 
3798                   ? parseInt(String(forcedActiveId)) 
3799                   : parseInt(String(window.activeGalleryId || activeGalleryId));
3800
3801  console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 
3802              (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 
3803              "Type:", typeof targetId);
3804
3805  const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item');
3806
3807  if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number
3808    console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId);
3809    return;
3810  }
3811
3812  let activeItemSet = false;
3813  menuItems.forEach(item => {
3814    const itemIdStr = item.dataset.id;
3815    if (!itemIdStr) {
3816      // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item);
3817      return; 
3818    }
3819    const itemId = parseInt(itemIdStr);
3820
3821    if (itemId === targetId) {
3822      if (!item.classList.contains('active')) {
3823        item.classList.add('active');
3824        console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
3825      }
3826      activeItemSet = true;
3827    } else {
3828      if (item.classList.contains('active')) {
3829        item.classList.remove('active');
3830        console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
3831      }
3832    }
3833  });
3834
3835  if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number
3836      console.warn(`[Horizontal Menu Update] Target Active ID was ${targetId}, but NO matching horizontal menu item was made active. Check data-id attributes and item visibility.`);
3837  }
3838
3839  const activeSubItem = document.querySelector('.horizontal-submenu-item.active');
3840  if (activeSubItem) {
3841      const parentTopItem = activeSubItem.closest('.horizontal-menu
3841-item.has-children');
3842      if (parentTopItem && !parentTopItem.classList.contains('active')) {
3843          // parentTopItem.classList.add('active-parent'); // Optional class for styling parent
3844      }
3845  }
3846  console.log("[Horizontal Menu Update] Completed for Target ID:", targetId);
3847}
3848
3849
3850  // Listener for closing dropdowns when clicking outside
3851  document.addEventListener('click', function(event) {
3852      if (currentMenuLayout === 'horizontal') {
3853          const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded');
3854          let clickedInsideSubmenuOrParent = false;
3855
3856          openSubmenus.forEach(submenuContainer => {
3857              // Check if click is on the submenu container OR its direct parent menu item
3858              if (submenuContainer.contains(event.target) || 
3859                  (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) {
3860                  clickedInsideSubmenuOrParent = true;
3861              }
3862          });
3863          
3864          // If click is on another top-level item that is NOT expanded, also don't close
3865          const targetMenuItem = event.target.closest('.horizontal-menu-item');
3866          if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) {
3867               clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it
3868          }
3869
3870
3871          if (!clickedInsideSubmenuOrParent) {
3872              openSubmenus.forEach(submenuContainer => {
3873                  submenuContainer.classList.remove('expanded');
3874              });
3875          }
3876      }
3877  });
3878
3879document.addEventListener('menuLayoutChanged', function(e) {
3880    const newLayout = e.detail.layout;
3881    console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing);
3882
3883    // Update the global currentMenuLayout variable if you have one
3884    if (typeof currentMenuLayout !== 'undefined') {
3885        currentMenuLayout = newLayout;
3886    }
3887
3888    const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected
3889
3890    // Always clear and re-render the appropriate menu structure for the new layout.
3891    // This is important for style changes (like font size) that affect dimensions.
3892    const galleryTreeContainer = document.getElementById('galleryTree');
3893    const mobileHeaderContent = document.querySelector('.mobile-header-content');
3894
3895    if (galleryTreeContainer) {
3896        galleryTreeContainer.innerHTML = ''; // Clear existing tree
3897    }
3898    if (mobileHeaderContent) {
3899        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
3900        if (horizontalMenuContainer) {
3901            horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself
3902        }
3903    }
3904
3905    // Render the correct menu structure based on the new layout
3906    if (newLayout === 'horizontal') {
3907        if (typeof renderHorizontalMenu === 'function') {
3908            renderHorizontalMenu();
3909        }
3910        // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal
3911    } else { // 'sidebar' or 'top'
3912        if (typeof renderGalleries === 'function') {
3913            renderGalleries();
3914        }
3915        // CSS rules will handle sidebar visibility
3916    }
3917
3918    // If currently in edit mode, ensure the UI components for editing are correctly displayed
3919    // for the new layout, without fully toggling the mode off and on.
3920    if (isEditing && typeof window.updateGlobalEditState === 'function') {
3921        console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.');
3922
3923        // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode.
3924        // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar.
3925        window.updateGlobalEditState(true);
3926
3927        // 2. Re-initialize sortables. It's often safer to destroy existing ones first.
3928        if (typeof initializeNestedSortables === 'function') {
3929            if (typeof destroyNestedSortables === 'function') {
3930                destroyNestedSortables();
3931            }
3932            setTimeout(initializeNestedSortables, 100); // Delay for DOM updates
3933        }
3934        // Also reinitialize for SidebarManager if it exists and handles its own sortables
3935        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
3936             setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150);
3937        }
3938
3939        // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
3940        const editControls = document.querySelector('.edit-controls');
3941        if (editControls) {
3942            editControls.style.display = 'flex'; // Or your default display type for these controls
3943            editControls.classList.add('visible');
3944        }
3945
3946        // 4. If the layout is now horizontal, the sidebar (for editing) should be visible.
3947        // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }`
3948        // should handle this. This log is for confirmation.
3949        if (newLayout === 'horizontal' && sidebar) {
3950            console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.');
3951        }
3952        
3953        // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set.
3954        if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 
3955            window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) {
3956            window.PageManager.setEditMode(true);
3957        }
3958
3959    } else if (!isEditing) {
3960        // If not in edit mode, CSS should handle the view state.
3961        // This block can be used for any explicit view mode adjustments if CSS isn't sufficient.
3962        console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.');
3963    }
3964
3965    // Update mobile title and close mobile menu if open, regardless of edit mode
3966    if (typeof updateMobileTitle === 'function') {
3967        updateMobileTitle();
3968    }
3969    if (typeof closeMobileMenu === 'function') {
3970        closeMobileMenu();
3971    }
3972
3973    // Re-render the active page so row-band insets and recipe layout match the new menu shell
3974    if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) {
3975        console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId);
3976        window.PageManager.renderElements(window.PageManager.currentPageId);
3977    }
3978    if (typeof ensureCorrectLayoutApplied === 'function') {
3979        setTimeout(ensureCorrectLayoutApplied, 0);
3980    }
3981});
3982
3983
3984// Make sure getFirstSidebarElementForHeader is available if called from other scripts
3985if (typeof window.getFirstSidebarElementForHeader === 'undefined') {
3986  window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader;
3987}
3988
3989// Make sure getFirstSocialElementForHeader is available if called from other scripts
3990if (typeof window.getFirstSocialElementForHeader === 'undefined') {
3991  window.getFirstSocialElementForHeader = getFirstSocialElementForHeader;
3992}
3993
3994// Make sure renderSocialIcons is available if called from other scripts
3995if (typeof window.renderSocialIcons === 'undefined') {
3996  window.renderSocialIcons = renderSocialIcons;
3997}
3998if (typeof window.renderHorizontalMenu === 'undefined') {
3999    window.renderHorizontalMenu = renderHorizontalMenu;
4000}
4001if (typeof window.createHorizontalMenuItem === 'undefined') {
4002    window.createHorizontalMenuItem = createHorizontalMenuItem;
4003}
4004if (typeof window.createHorizontalSubMenuItem === 'undefined') {
4005    window.createHorizontalSubMenuItem = createHorizontalSubMenuItem;
4006}
4007if (typeof window.updateActiveStatesHorizontal === 'undefined') {
4008    window.updateActiveStatesHorizontal = updateActiveStatesHorizontal;
4009}
4010// Ensure toggleEditMode is globally available if it's the primary one.
4011if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder
4012    window.toggleEditMode = toggleEditMode;
4013}
4014
4015
4016// Helper function to toggle edit controls visibility
4017function toggleEditControlsVisibility(show) {
4018  const editControls = document.querySelector('.edit-controls');
4019  if (editControls) {
4020    if (show) {
4021      editControls.classList.add('visible');
4022      editControls.style.display = 'flex';
4023    } else {
4024      editControls.classList.remove('visible');
4025      editControls.style.display = 'none';
4026    }
4027  }
4028}
4029
4030/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */
4031function isHydraPlusKey(e) {
4032  if (e.repeat) return false;
4033  if (e.ctrlKey || e.metaKey || e.altKey) return false;
4034  if (e.key === '+' || e.key === 'Add') return true;
4035  if (e.code === 'NumpadAdd') return true;
4036  if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true;
4037  if (e.keyCode === 187 && e.shiftKey) return true;
4038  if (e.keyCode === 107) return true;
4039  return false;
4040}
4041
4042function handleHydraPlusShortcut() {
4043  try {
4044    const token = window.didToken || localStorage.getItem('hydra_auth_token');
4045    const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
4046    if (token && isAdmin) {
4047      console.log('User already logged in - toggling edit mode');
4048      if (typeof window.toggleEditMode === 'function') {
4049        window.toggleEditMode();
4050      } else if (typeof toggleEditMode === 'function') {
4051        toggleEditMode();
4052      } else {
4053        console.warn('toggleEditMode function not found');
4054      }
4055    } else {
4056      const p = startOTPLogin();
4057      if (p && typeof p.then === 'function') {
4058        p.catch(function(err) {
4059          console.error('startOTPLogin failed:', err);
4060        });
4061      }
4062    }
4063  } catch (err) {
4064    console.error('Hydra + key handler error:', err);
4065  }
4066}
4067
4068document.addEventListener('keydown', function(e) {
4069  if (!isHydraPlusKey(e)) return;
4070  e.preventDefault();
4071  handleHydraPlusShortcut();
4072}, true);
4073
4074// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage.
4075function isHydraCustomPageHtmlPlusMessage(e) {
4076  if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false;
4077  if (e.origin === window.location.origin) return true;
4078  // Sandboxed iframes (no allow-same-origin) post with origin "null" — accept only from our iframe.
4079  if (e.origin !== 'null' || !e.source) return false;
4080  const iframes = document.querySelectorAll('iframe.page-custom-html-iframe');
4081  for (let i = 0; i < iframes.length; i++) {
4082    if (iframes[i].contentWindow === e.source) return true;
4083  }
4084  return false;
4085}
4086
4087window.addEventListener('message', function(e) {
4088  if (!isHydraCustomPageHtmlPlusMessage(e)) return;
4089  handleHydraPlusShortcut();
4090});
4091
4092/** Option+/ (÷) opens Help → Pathways. Option+? opens Help → Ask (help admin only). */
4093function isHydraHelpKey(e) {
4094  if (e.repeat) return false;
4095  if (e.ctrlKey || e.metaKey) return false;
4096  if (!e.altKey) return false;
4097  if (e.shiftKey) return false;
4098  if (e.key === '÷') return true;
4099  if (e.code === 'Slash' && e.altKey) return true;
4100  return false;
4101}
4102
4103function isHydraHelpAskKey(e) {
4104  if (e.repeat) return false;
4105  if (e.ctrlKey || e.metaKey) return false;
4106  if (!e.altKey || !e.shiftKey) return false;
4107  if (e.key === '?' || e.key === '¿') return true;
4108  if (e.code === 'Slash') return true;
4109  return false;
4110}
4111
4112document.addEventListener('keydown', function(e) {
4113  if (!window.HYDRA_HELP_ENABLED) return;
4114  var t = e.target;
4115  if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
4116  if (isHydraHelpAskKey(e)) {
4117    e.preventDefault();
4118    e.stopPropagation();
4119    openHydraHelp({ initialTab: 'ask' });
4120    return;
4121  }
4122  if (!isHydraHelpKey(e)) return;
4123  e.preventDefault();
4124  e.stopPropagation();
4125  openHydraHelp();
4126}, true);
4127
4128// Email OTP Authentication with Enhanced Debugging
4129
4130
4131// Add this function to the client-side code
4132// Improved client-side backup function with better token handling
4133async function createLoginBackup() {
4134  try {
4135    console.log('Attempting to create login backup...');
4136    
4137    // Get authentication info from multiple possible sources
4138    let token = window.didToken || localStorage.getItem('hydra_auth_token');
4139    const email = localStorage.getItem('hydra_auth_email') || 
4140                  (window.userMetadata ? window.userMetadata.email : '') ||
4141                  '';
4142    
4143    // Check if we have authentication
4144    if (!token) {
4145      console.error('No auth token available for backup');
4146      return;
4147    }
4148    
4149    if (!email) {
4150      console.error('No email available for backup - make sure it was stored during login');
4151      return;
4152    }
4153    
4154    console.log(`Creating backup with token (${token.length} chars) for ${email}`);
4155    
4156    // Format the token correctly - VERY IMPORTANT
4157    // If it's already a hydra token (starts with hydra:), use it as is
4158    // Otherwise, add the hydra: prefix
4159    const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`;
4160    
4161    console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`);
4162    
4163    // Get the API URL with preview handling if needed
4164    const backupUrl = typeof getApiUrl === 'function' ? 
4165      getApiUrl('/api/create-backup') : 
4166      '/api/create-backup';
4167    
4168    console.log(`Sending backup request to: ${backupUrl}`);
4169    
4170    // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header
4171    const response = await fetch(backupUrl, {
4172      method: 'POST',
4173      headers: {
4174        'Authorization': `Bearer ${formattedToken}`,
4175        'Content-Type': 'application/json',
4176        'X-User-Email': email, // Include email as fallback
4177        'X-API-Request': 'true' // Mark as API request
4178      }
4179    });
4180    
4181    // Handle response
4182    if (!response.ok) {
4183      const errorText = await response.text();
4184      let errorData;
4185      try {
4186        // Try to parse as JSON
4187        errorData = JSON.parse(errorText);
4188        console.error('Backup failed:', errorData.error, errorData);
4189      } catch {
4190        // Not JSON, log raw text
4191        console.error('Backup failed with status', response.status, errorText);
4192      }
4193      return;
4194    }
4195    
4196    // Parse successful response
4197    const result = await response.json();
4198    console.log(`✅ Backup created successfully: ${result.backup}`);
4199  } catch (error) {
4200    console.error('Error in backup process:', error);
4201  }
4202}
4203
4204// Initialize Magic with detailed logging
4205function initMagic() {
4206  try {
4207    // If Magic is already initialized, return it
4208    if (window.magic) {
4209      return window.magic;
4210    }
4211    
4212    console.log('Initializing Magic SDK...');
4213    
4214    // Initialize with minimal options
4215    const magic = new Magic('pk_live_E0B68BA6DCA8C75F', {
4216      testMode: false
4217    });
4218    
4219    // Store reference globally
4220    window.magic = magic;
4221    
4222    console.log('Magic SDK initialized successfully');
4223    return magic;
4224  } catch (error) {
4225    console.error('Error initializing Magic:', error);
4226    throw error;
4227  }
4228}
4229
4230// Start OTP login flow with proper event handling
4231async function startOTPLogin() {
4232  try {
4233    // Initialize Magic
4234    const magic = initMagic();
4235    
4236    // Get email from user (styled overlay — same auth path as native prompt)
4237    const email = await showEmailLoginOverlay();
4238    if (!email) return; // User cancelled
4239    
4240    console.log('Starting Email OTP login for:', email);
4241    showLoadingOverlay('Sending verification code...');
4242    
4243    // Check if Magic's event enum types are available - for newer Magic SDK versions
4244    // If not available, fall back to string-based events
4245    const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 
4246                    typeof LoginWithEmailOTPEventEmit !== 'undefined';
4247    
4248    console.log('Using enum-based events:', useEnums);
4249    
4250    // Create a timeout handler
4251    let emailSentTimeout = setTimeout(() => {
4252      console.log('No email-otp-sent event received, using fallback UI');
4253      hideLoadingOverlay();
4254      showOTPInputOverlay(email);
4255    }, 10000);
4256    
4257    // Create OTP login handle with showUI: false
4258    const handle = magic.auth.loginWithEmailOTP({ 
4259      email, 
4260      showUI: false,
4261      deviceCheckUI: false
4262    });
4263    
4264    // Track authentication state
4265    window.pendingAuth = {
4266      email,
4267      handle
4268    };
4269    
4270    // Listen for events - using both enum-based and string-based event handling
4271    console.log('Setting up OTP event handlers...');
4272    
4273    // Email OTP sent event
4274    if (useEnums) {
4275      handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => {
4276        console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully');
4277        clearTimeout(emailSentTimeout);
4278        hideLoadingOverlay();
4279        showOTPInputOverlay(email);
4280      });
4281    }
4282    
4283    // Also listen for string-based event (for backward compatibility)
4284    handle.on('email-otp-sent', () => {
4285      console.log('EVENT: email-otp-sent - Email was sent successfully');
4286      clearTimeout(emailSentTimeout);
4287      hideLoadingOverlay();
4288      showOTPInputOverlay(email);
4289    });
4290    
4291    // Invalid OTP event
4292    if (useEnums) {
4293      handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => {
4294        console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered');
4295        showErrorMessage('Invalid verification code. Please try again.');
4296        updateOTPInputForRetry();
4297      });
4298    }
4299    
4300    // Also listen for string-based event
4301    handle.on('invalid-email-otp', () => {
4302      console.log('EVENT: invalid-email-otp - Invalid code entered');
4303      showErrorMessage('Invalid verification code. Please try again.');
4304      updateOTPInputForRetry();
4305    });
4306    
4307    async function handleLoginSuccess(result) {
4308  console.log('OTP verification successful, checking admin status...');
4309  hideOTPInputOverlay();
4310  hideLoadingOverlay();
4311
4312  try {
4313    window.didToken = result;
4314    console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none');
4315
4316    const userInfo = await magic.user.getInfo();
4317    window.userMetadata = userInfo;
4318    console.log('User info retrieved:', userInfo);
4319
4320    const isUserAdmin = await checkAdminStatus(result);
4321
4322    if (!isUserAdmin && !window.isAdmin) {
4323      console.log('User is not on admin list, login denied');
4324      showErrorMessage('Login failed');
4325      clearHydraClientAuthStorage();
4326      resetAuthUI();
4327      try {
4328        await magic.user.logout();
4329      } catch (logoutError) {
4330        console.error('Error logging out non-admin user from Magic:', logoutError);
4331      }
4332      window.pendingAuth = null;
4333      return;
4334    }
4335
4336    console.log('Admin status check completed');
4337    window.isAuthenticated = true;
4338    isAuthenticated = true;
4339    showSuccessMessage('Login successful!');
4340
4341    // Store the ID token (JWT) for API calls
4342    try {
4343      const idToken = await magic.user.getIdToken();
4344      if (idToken && idToken.includes('.')) {
4345        console.log('Got proper JWT token from Magic.link');
4346        localStorage.setItem('hydra_auth_token', idToken);
4347      } else if (userInfo && userInfo.email) {
4348        const userToken = {
4349          email: userInfo.email,
4350          iat: Math.floor(Date.now() / 1000),
4351          exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60)
4352        };
4353        const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' }));
4354        const payload = btoa(JSON.stringify(userToken));
4355        const jwtToken = header + '.' + payload + '.nosignature';
4356        localStorage.setItem('hydra_auth_token', jwtToken);
4357        localStorage.setItem('hydra_auth_email', userInfo.email);
4358      }
4359      localStorage.setItem('hydra_did_token', result);
4360    } catch (tokenError) {
4361      console.error('Error getting ID token:', tokenError);
4362      localStorage.setItem('hydra_auth_token', result);
4363    }
4364
4365    // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy
4366
4367    // 1. Add state classes to body
4368    document.body.classList.add('hydra-authenticated');
4369    if (window.isAdmin) {
4370      document.body.classList.add('hydra-admin');
4371    }
4372
4373    // 2. Direct DOM manipulation
4374    const logoutButton = document.getElementById('logoutButton');
4375    const editButton = document.getElementById('editButton');
4376
4377    // Create new buttons if they don't exist
4378    if (!logoutButton) {
4379      console.log('Creating logout button');
4380      const newLogoutButton = document.createElement('button');
4381      newLogoutButton.id = 'logoutButton';
4382      newLogoutButton.className = 'btn';
4383      newLogoutButton.textContent = 'Logout';
4384      newLogoutButton.onclick = logout;
4385      newLogoutButton.style.display = 'block';
4386
4387      const sidebarHeader = document.querySelector('.sidebar-header');
4388      if (sidebarHeader) {
4389        sidebarHeader.appendChild(newLogoutButton);
4390      } else {
4391        document.body.appendChild(newLogoutButton);
4392      }
4393    } else {
4394      logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4395    }
4396
4397    if (!editButton && window.isAdmin) {
4398      console.log('Creating edit button');
4399      const newEditButton = document.createElement('button');
4400      newEditButton.id = 'editButton';
4401      newEditButton.className = 'btn btn-primary';
4402      newEditButton.innerHTML = `
4403        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4404          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
4405          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
4406        </svg>
4407        Edit
4408      `;
4409      newEditButton.onclick = toggleEditMode;
4410      newEditButton.style.display = 'block';
4411
4412      const sidebarHeader = document.querySelector('.sidebar-header');
4413      if (sidebarHeader) {
4414        sidebarHeader.appendChild(newEditButton);
4415      } else {
4416        document.body.appendChild(newEditButton);
4417      }
4418    } else if (editButton && window.isAdmin) {
4419      editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4420    }
4421
4422    // 3. Dispatch event for other listeners
4423    document.dispatchEvent(new CustomEvent('login-complete', {
4424      detail: {
4425        isAdmin: window.isAdmin,
4426        email: window.userMetadata ? window.userMetadata.email : null
4427      }
4428    }));
4429
4430    window.pendingAuth = null;
4431
4432    if (typeof checkAuth === 'function') {
4433      console.log('Running existing checkAuth function');
4434      setTimeout(checkAuth, 500);
4435    }
4436  } catch (error) {
4437    console.error('Error in login success handling:', error);
4438    showErrorMessage('Login failed');
4439    clearHydraClientAuthStorage();
4440    resetAuthUI();
4441    window.pendingAuth = null;
4442  }
4443}
4444
4445
4446    // Completion event
4447    handle.on('done', async (result) => {
4448      await handleLoginSuccess(result);
4449    });
4450    
4451    // Error event
4452    handle.on('error', (error) => {
4453      console.error('EVENT: error - OTP login error:', error);
4454      hideLoadingOverlay();
4455      hideOTPInputOverlay();
4456      showErrorMessage('Login failed: ' + (error.message || 'Unknown error'));
4457      
4458      // Clear timeout if still active
4459      clearTimeout(emailSentTimeout);
4460      
4461      // Clean up
4462      window.pendingAuth = null;
4463    });
4464    
4465    console.log('OTP event handlers set up successfully');
4466    
4467  } catch (error) {
4468    console.error('Error starting OTP login:', error);
4469    hideLoadingOverlay();
4470    hideOTPInputOverlay();
4471    showErrorMessage('Error starting login: ' + error.message);
4472  }
4473}
4474
4475// Submit OTP code with support for enum-based events
4476function submitOTP(code) {
4477  try {
4478    // Validate code input
4479    if (!code || code.trim() === '') {
4480      showErrorMessage('Please enter the verification code.');
4481      return;
4482    }
4483    
4484    // Check if we have a pending authentication
4485    if (!window.pendingAuth || !window.pendingAuth.handle) {
4486      showErrorMessage('No active login session. Please try again.');
4487      hideOTPInputOverlay();
4488      return;
4489    }
4490    
4491    console.log('Submitting OTP code...');
4492    showLoadingOverlay('Verifying code...');
4493    
4494    // Check if enum types are available
4495    const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4496    
4497    // Verify the OTP code
4498    if (useEnums) {
4499      window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code);
4500    } else {
4501      window.pendingAuth.handle.emit('verify-email-otp', code);
4502    }
4503    
4504  } catch (error) {
4505    console.error('Error submitting OTP:', error);
4506    hideLoadingOverlay();
4507    showErrorMessage('Error verifying code: ' + error.message);
4508  }
4509}
4510
4511// Cancel OTP login with support for enum-based events
4512function cancelOTPLogin() {
4513  try {
4514    // Check if we have a pending authentication
4515    if (window.pendingAuth && window.pendingAuth.handle) {
4516      // Check if enum types are available
4517      const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4518      
4519      // Emit cancel event
4520      if (useEnums) {
4521        window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel);
4522      } else {
4523        window.pendingAuth.handle.emit('cancel');
4524      }
4525      
4526      console.log('OTP login cancelled');
4527    }
4528    
4529    // Clean up
4530    window.pendingAuth = null;
4531    
4532    // Hide any overlays
4533    hideEmailLoginOverlay();
4534    hideOTPInputOverlay();
4535    hideLoadingOverlay();
4536  } catch (error) {
4537    console.error('Error cancelling OTP login:', error);
4538  }
4539}
4540
4541// Update OTP input for retry
4542function updateOTPInputForRetry() {
4543  const otpInput = document.getElementById('otp-input');
4544  if (otpInput) {
4545    otpInput.value = '';
4546    otpInput.focus();
4547    
4548    // Add a shake animation for visual feedback
4549    otpInput.classList.add('shake');
4550    
4551    // Remove the animation class after it completes
4552    setTimeout(() => {
4553      otpInput.classList.remove('shake');
4554    }, 500);
4555  }
4556  
4557  // Hide loading overlay if it's visible
4558  hideLoadingOverlay();
4559}
4560
4561function isValidLoginEmail(value) {
4562  const trimmed = String(value || '').trim();
4563  if (!trimmed || trimmed.indexOf(' ') >= 0) return false;
4564  const at = trimmed.indexOf('@');
4565  if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false;
4566  const local = trimmed.slice(0, at);
4567  const domain = trimmed.slice(at + 1);
4568  if (!local || !domain || domain.indexOf('.') < 1) return false;
4569  if (domain.startsWith('.') || domain.endsWith('.')) return false;
4570  return true;
4571}
4572
4573function showEmailLoginOverlay() {
4574  hideEmailLoginOverlay();
4575
4576  return new Promise((resolve) => {
4577    const overlay = document.createElement('div');
4578    overlay.id = 'email-login-overlay';
4579    overlay.className = 'pe-auth-overlay';
4580
4581    const content = document.createElement('div');
4582    content.className = 'pe-auth-shell';
4583
4584    const logo = document.createElement('img');
4585    logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4586    logo.alt = 'Neon Sky Logo';
4587    logo.className = 'pe-auth-shell__logo';
4588
4589    const description = document.createElement('p');
4590    description.className = 'pe-auth-shell__desc';
4591    description.textContent = 'Enter your email address to receive a verification code.';
4592
4593    const form = document.createElement('form');
4594    form.noValidate = true;
4595    form.onsubmit = function(e) {
4596      e.preventDefault();
4597      const raw = input.value.trim();
4598      if (!isValidLoginEmail(raw)) {
4599        input.classList.add('shake');
4600        setTimeout(() => input.classList.remove('shake'), 500);
4601        return;
4602      }
4603      hideEmailLoginOverlay();
4604      resolve(raw);
4605    };
4606
4607    const input = document.createElement('input');
4608    input.id = 'email-login-input';
4609    input.type = 'email';
4610    input.name = 'email';
4611    input.placeholder = '[email protected]';
4612    input.autocomplete = 'email';
4613    input.inputMode = 'email';
4614    input.required = true;
4615    input.className = 'pe-auth-shell__input pe-auth-shell__input--email';
4616    const savedEmail = localStorage.getItem('hydra_auth_email');
4617    if (savedEmail && isValidLoginEmail(savedEmail)) {
4618      input.value = savedEmail.trim();
4619    }
4620    form.appendChild(input);
4621
4622    const buttonContainer = document.createElement('div');
4623    buttonContainer.className = 'pe-auth-actions';
4624
4625    const continueButton = document.createElement('button');
4626    continueButton.type = 'submit';
4627    continueButton.textContent = 'Continue';
4628    continueButton.className = 'pe-btn pe-btn--primary';
4629    buttonContainer.appendChild(continueButton);
4630
4631    const cancelButton = document.createElement('button');
4632    cancelButton.type = 'button';
4633    cancelButton.textContent = 'Cancel';
4634    cancelButton.className = 'pe-btn';
4635    cancelButton.onclick = function() {
4636      hideEmailLoginOverlay();
4637      resolve(null);
4638    };
4639    buttonContainer.appendChild(cancelButton);
4640    form.appendChild(buttonContainer);
4641
4642    content.appendChild(logo);
4643    content.appendChild(description);
4644    content.appendChild(form);
4645    overlay.appendChild(content);
4646    document.body.appendChild(overlay);
4647
4648    setTimeout(() => {
4649      input.focus();
4650      if (input.value) {
4651        input.select();
4652      }
4653    }, 100);
4654  });
4655}
4656
4657function hideEmailLoginOverlay() {
4658  const overlay = document.getElementById('email-login-overlay');
4659  if (overlay && overlay.parentNode) {
4660    overlay.parentNode.removeChild(overlay);
4661  }
4662}
4663
4664// Show OTP input overlay - improved version
4665function showOTPInputOverlay(email) {
4666  hideOTPInputOverlay();
4667  hideEmailLoginOverlay();
4668
4669  const overlay = document.createElement('div');
4670  overlay.id = 'otp-input-overlay';
4671  overlay.className = 'pe-auth-overlay';
4672
4673  const content = document.createElement('div');
4674  content.className = 'pe-auth-shell';
4675
4676  const logo = document.createElement('img');
4677  logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4678  logo.alt = 'Neon Sky Logo';
4679  logo.className = 'pe-auth-shell__logo';
4680
4681  const description = document.createElement('p');
4682  description.className = 'pe-auth-shell__desc';
4683  const descPrefix = document.createTextNode("We've sent a verification code to ");
4684  const descEmail = document.createElement('strong');
4685  descEmail.textContent = String(email || '');
4686  const descSuffix = document.createTextNode('. Please check your email and enter the code below:');
4687  description.appendChild(descPrefix);
4688  description.appendChild(descEmail);
4689  description.appendChild(document.createElement('br'));
4690  description.appendChild(descSuffix);
4691
4692  const form = document.createElement('form');
4693  form.onsubmit = function(e) {
4694    e.preventDefault();
4695    const otpInput = document.getElementById('otp-input');
4696    if (otpInput && otpInput.value) {
4697      submitOTP(otpInput.value);
4698    }
4699  };
4700
4701  const input = document.createElement('input');
4702  input.id = 'otp-input';
4703  input.type = 'text';
4704  input.placeholder = 'Enter code';
4705  input.pattern = '[0-9]*';
4706  input.inputMode = 'numeric';
4707  input.autocomplete = 'one-time-code';
4708  input.className = 'pe-auth-shell__input';
4709  form.appendChild(input);
4710
4711  const buttonContainer = document.createElement('div');
4712  buttonContainer.className = 'pe-auth-actions';
4713
4714  const verifyButton = document.createElement('button');
4715  verifyButton.type = 'submit';
4716  verifyButton.textContent = 'Verify';
4717  verifyButton.className = 'pe-btn pe-btn--primary';
4718  buttonContainer.appendChild(verifyButton);
4719
4720  const cancelButton = document.createElement('button');
4721  cancelButton.type = 'button';
4722  cancelButton.textContent = 'Cancel';
4723  cancelButton.className = 'pe-btn';
4724  cancelButton.onclick = function() {
4725    cancelOTPLogin();
4726  };
4727  buttonContainer.appendChild(cancelButton);
4728  form.appendChild(buttonContainer);
4729
4730  const resendContainer = document.createElement('div');
4731  resendContainer.className = 'pe-auth-resend';
4732
4733  const resendText = document.createElement('span');
4734  resendText.textContent = "Didn't receive the code? ";
4735
4736  const resendLink = document.createElement('a');
4737  resendLink.textContent = 'Resend';
4738  resendLink.href = '#';
4739  resendLink.onclick = function(e) {
4740    e.preventDefault();
4741    cancelOTPLogin();
4742    setTimeout(() => {
4743      startOTPLogin();
4744    }, 500);
4745  };
4746
4747  resendContainer.appendChild(resendText);
4748  resendContainer.appendChild(resendLink);
4749  form.appendChild(resendContainer);
4750
4751  content.appendChild(logo);
4752  content.appendChild(description);
4753  content.appendChild(form);
4754  overlay.appendChild(content);
4755  document.body.appendChild(overlay);
4756
4757  setTimeout(() => {
4758    input.focus();
4759  }, 100);
4760}
4761
4762// Hide OTP input overlay
4763function hideOTPInputOverlay() {
4764  const overlay = document.getElementById('otp-input-overlay');
4765  if (overlay && overlay.parentNode) {
4766    overlay.parentNode.removeChild(overlay);
4767  }
4768}
4769
4770// Utility function to show loading overlay
4771function showLoadingOverlay(message = 'Loading...') {
4772  hideLoadingOverlay();
4773
4774  const overlay = document.createElement('div');
4775  overlay.id = 'loading-overlay';
4776  overlay.className = 'pe-auth-overlay';
4777
4778  const content = document.createElement('div');
4779  content.className = 'pe-auth-shell';
4780
4781  const loaderContainer = document.createElement('div');
4782  loaderContainer.className = 'pe-auth-shell__loader';
4783  loaderContainer.innerHTML = `
4784    <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
4785      <defs>
4786        <filter id="svgSpinnersGooeyBalls20">
4787          <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
4788          <feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/>
4789          <feBlend in="SourceGraphic" in2="z"/>
4790        </filter>
4791      </defs>
4792      <g filter="url(#svgSpinnersGooeyBalls20)">
4793        <circle cx="5" cy="12" r="4" fill="currentColor">
4794          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
4795        </circle>
4796        <circle cx="19" cy="12" r="4" fill="currentColor">
4797          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
4798        </circle>
4799        <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
4800      </g>
4801    </svg>
4802  `;
4803  content.appendChild(loaderContainer);
4804
4805  const messageEl = document.createElement('p');
4806  messageEl.className = 'pe-auth-shell__message';
4807  messageEl.textContent = message;
4808  content.appendChild(messageEl);
4809
4810  overlay.appendChild(content);
4811  document.body.appendChild(overlay);
4812}
4813
4814// Utility function to hide loading overlay
4815function hideLoadingOverlay() {
4816  const overlay = document.getElementById('loading-overlay');
4817  if (overlay && overlay.parentNode) {
4818    overlay.parentNode.removeChild(overlay);
4819  }
4820}
4821
4822// Utility function to show success message
4823function showSuccessMessage(message, duration = 3000) {
4824  const existingToast = document.getElementById('success-toast');
4825  if (existingToast && existingToast.parentNode) {
4826    existingToast.parentNode.removeChild(existingToast);
4827  }
4828
4829  const toast = document.createElement('div');
4830  toast.id = 'success-toast';
4831  toast.className = 'pe-auth-toast';
4832  toast.textContent = message;
4833
4834  document.body.appendChild(toast);
4835
4836  setTimeout(() => {
4837    if (toast.parentNode) {
4838      toast.parentNode.removeChild(toast);
4839    }
4840  }, duration);
4841}
4842
4843// Utility function to show error message
4844function showErrorMessage(message, duration = 5000) {
4845  const existingToast = document.getElementById('error-toast');
4846  if (existingToast && existingToast.parentNode) {
4847    existingToast.parentNode.removeChild(existingToast);
4848  }
4849
4850  const toast = document.createElement('div');
4851  toast.id = 'error-toast';
4852  toast.className = 'pe-auth-toast pe-auth-toast--error';
4853  toast.textContent = message;
4854
4855  document.body.appendChild(toast);
4856
4857  setTimeout(() => {
4858    if (toast.parentNode) {
4859      toast.parentNode.removeChild(toast);
4860    }
4861  }, duration);
4862}
4863
4864// UPDATED: Check authentication status with class-based approach
4865async function checkAuth() {
4866  try {
4867    if (!magic) {
4868      console.log('Magic not initialized, skipping auth check');
4869      return;
4870    }
4871    
4872    // Prevent multiple simultaneous auth checks
4873    if (window._checkingAuth) return;
4874    window._checkingAuth = true;
4875    
4876    console.log('Checking auth status...');
4877    
4878    isAuthenticated = await magic.user.isLoggedIn();
4879    console.log('isLoggedIn check:', isAuthenticated);
4880    
4881    if (isAuthenticated) {
4882      // Add the authenticated class 
4883      document.body.classList.add('hydra-authenticated');
4884      
4885      // Get user metadata
4886      userMetadata = await magic.user.getInfo();
4887      console.log('User info:', userMetadata);
4888      
4889      // Get ID token for API calls (JWT format)
4890      const rawDidToken = await magic.user.getIdToken();
4891      console.log('ID token acquired, length:', rawDidToken.length);
4892      
4893      // Do NOT write the raw DID token to localStorage here — checkAdminStatus will
4894      // receive the server-issued hydra token and store that instead, which is what
4895      // the save function needs. Writing the DID token now would overwrite a valid
4896      // hydra token that may already be stored from the OTP login flow.
4897      
4898      // Check if user is admin; on success this updates both localStorage and didToken
4899      // with a proper hydra token via checkAdminStatus → TokenManager.storeToken
4900      await checkAdminStatus(rawDidToken);
4901    } else {
4902      // Magic has no session — still try Hydra OTP token so /_manage and return visits work
4903      const hydraOnly = localStorage.getItem('hydra_auth_token');
4904      if (hydraOnly) {
4905        try {
4906          await checkAdminStatus(hydraOnly);
4907        } catch (e) {
4908          console.error('Hydra token restore after Magic logout failed:', e);
4909        }
4910      } else {
4911        resetAuthUI();
4912      }
4913    }
4914    
4915    // Release lock
4916    window._checkingAuth = false;
4917  } catch (error) {
4918    console.error('Error checking authentication:', error);
4919    window._checkingAuth = false;
4920    resetAuthUI();
4921  }
4922}
4923
4924
4925async function checkAdminStatus(token) {
4926  try {
4927    console.log('Checking admin status...');
4928
4929    // Add user email to headers if available
4930    let userEmail = null;
4931    if (window.userMetadata && window.userMetadata.email) {
4932      userEmail = window.userMetadata.email;
4933      console.log('Using email from userMetadata:', userEmail);
4934    } else {
4935        userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage
4936        if (userEmail) console.log('Using email from localStorage:', userEmail);
4937    }
4938
4939    if (!token) {
4940      console.log('No token provided to checkAdminStatus');
4941      return false; // Indicate failure
4942    }
4943
4944    // Format the token correctly for the API call
4945    let formattedToken = token;
4946    // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix
4947    if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) {
4948        formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer
4949    } else if (token.startsWith('hydra:')) {
4950        formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens
4951    }
4952    // If it already starts with Bearer, use as is
4953
4954    // Get the API URL, handling preview mode
4955    const apiUrl = typeof getApiUrl === 'function' ?
4956      getApiUrl('/api/check-admin') :
4957      '/api/check-admin';
4958
4959    console.log('Sending admin check request to:', apiUrl);
4960
4961    // Create request headers
4962    const headers = {
4963      'Authorization': formattedToken,
4964      'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow
4965    };
4966
4967    if (userEmail) {
4968      headers['X-User-Email'] = userEmail;
4969    }
4970
4971    console.log('Admin check headers:', {
4972      'Authorization': `${formattedToken.substring(0, 20)}...`,
4973      'X-User-Email': userEmail || 'Not available'
4974    });
4975
4976    // Add timeout to the fetch request
4977    const controller = new AbortController();
4978    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
4979
4980    let data;
4981    try {
4982      const response = await fetch(apiUrl, {
4983        method: 'GET',
4984        headers: headers,
4985        signal: controller.signal
4986      });
4987
4988      clearTimeout(timeoutId); // Clear timeout if fetch succeeds
4989
4990      console.log('Admin check response status:', response.status);
4991
4992      // Get the response as text first for better debugging
4993      const responseText = await response.text();
4994      console.log('Admin check response length:', responseText.length);
4995      console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...');
4996
4997
4998      try {
4999        data = JSON.parse(responseText);
5000        console.log('Admin check data:', data);
5001      } catch (parseError) {
5002        console.error('Error parsing admin check response:', parseError);
5003        // Handle cases where the response might not be JSON (e.g., HTML error page)
5004        // For preview URLs, let's still assume admin to allow editing flow
5005        if (window.location.hostname === 'preview.neonsky.app') {
5006          console.log('Preview URL detected, assuming admin status due to non-JSON response');
5007          isAdmin = true;
5008          document.body.classList.add('hydra-admin');
5009          localStorage.setItem('hydra_is_admin', 'true');
5010          forceShowEditUI(); // Attempt to show UI
5011          return true; // Indicate potential success for preview
5012        }
5013        throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`);
5014      }
5015
5016      if (!response.ok) {
5017          // Throw an error with details from the parsed JSON if available
5018          throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`);
5019      }
5020
5021      // Process the admin status
5022      isAdmin = data.isAdmin;
5023      window.isAdmin = isAdmin; // Update global flag
5024
5025      // Update classes based on admin status
5026      if (isAdmin) {
5027        document.body.classList.add('hydra-admin');
5028        localStorage.setItem('hydra_is_admin', 'true');
5029        // Store email if we got it from the server response
5030        if (data.email) {
5031            localStorage.setItem('hydra_auth_email', data.email);
5032        } else if (userEmail) {
5033            localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used
5034        }
5035      } else {
5036        document.body.classList.remove('hydra-admin');
5037        localStorage.removeItem('hydra_is_admin');
5038        localStorage.removeItem('hydra_auth_email'); // Clear email if not admin
5039      }
5040
5041      // Update siteId if provided
5042      if (data.siteId) {
5043        siteId = data.siteId;
5044        window.siteId = siteId;
5045        if (window.Parameters) window.Parameters.siteId = siteId;
5046      }
5047
5048      // Store new token if provided (Hydra token)
5049      if (data.hydraToken) {
5050        console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...');
5051        const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken;
5052        // Always update the local didToken variable so saves immediately use the hydra token
5053        didToken = cleanHydraToken;
5054        // Store token globally and in localStorage using TokenManager if available
5055        if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
5056            window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || '');
5057        } else {
5058            // Fallback storage
5059            localStorage.setItem('hydra_auth_token', cleanHydraToken);
5060            if (data.email || userEmail) {
5061                localStorage.setItem('hydra_auth_email', data.email || userEmail);
5062            }
5063        }
5064        console.log('Stored credentials in localStorage');
5065      }
5066
5067      // Update UI based on admin status
5068      if (isAdmin) {
5069        console.log('Admin status confirmed, showing edit UI');
5070        forceShowEditUI(); // Ensure UI elements are visible
5071        // --- MODIFICATION START ---
5072        // If layout is horizontal, re-render the menu to show controls
5073        let currentLayout = 'sidebar'; // Default
5074        if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5075            currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5076        }
5077        if (currentLayout === 'horizontal') {
5078            console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.');
5079            if (typeof renderHorizontalMenu === 'function') {
5080                renderHorizontalMenu();
5081            } else {
5082                console.warn("checkAdminStatus: renderHorizontalMenu function not found.");
5083            }
5084        }
5085        // --- MODIFICATION END ---
5086        createLoginBackup(); // Create backup after successful login/admin check
5087      } else {
5088          console.log('User is not admin, resetting UI.');
5089          resetAuthUI(); // Ensure non-admin UI state
5090      }
5091
5092      return isAdmin; // Return the admin status
5093
5094    } catch (fetchError) {
5095      clearTimeout(timeoutId); // Clear timeout on error as well
5096      console.error('Fetch error during admin check:', fetchError);
5097
5098      // For preview URLs, still allow editing as a fallback
5099      if (window.location.hostname === 'preview.neonsky.app') {
5100        console.log('Preview URL + fetch error, still enabling editing');
5101        isAdmin = true;
5102        window.isAdmin = true;
5103        document.body.classList.add('hydra-admin');
5104        localStorage.setItem('hydra_is_admin', 'true');
5105        forceShowEditUI();
5106        return true; // Indicate potential success for preview
5107      }
5108
5109      // For non-preview, throw the error to indicate failure
5110      throw fetchError;
5111    }
5112  } catch (error) {
5113    console.error('Error in checkAdminStatus:', error);
5114    // Don't show alert here, let the calling function handle UI feedback
5115    // Reset UI to non-admin state on error
5116    resetAuthUI();
5117    return false; // Indicate failure
5118  }
5119}
5120
5121// UPDATED: Force showing edit UI with class-based approach
5122function forceShowEditUI() {
5123  console.log('Forcing admin/auth state for UI');
5124
5125  // Ensure global state flags are set
5126  isAuthenticated = true;
5127  isAdmin = true;
5128  window.isAuthenticated = true;
5129  window.isAdmin = true;
5130
5131  // Add admin/auth classes to body - CSS will handle button visibility
5132  document.body.classList.add('hydra-authenticated', 'hydra-admin');
5133
5134  // Store admin status in localStorage
5135  localStorage.setItem('hydra_is_admin', 'true');
5136
5137  // Show edit controls container ONLY if already in edit mode
5138  if (window.isInEditMode && window.isInEditMode()) {
5139    const editControls = document.querySelector('.edit-controls');
5140    if (editControls) {
5141      editControls.classList.add('visible');
5142      editControls.style.display = 'flex'; // Make sure container is flex
5143    }
5144    // Ensure controls inside items are visible if already editing
5145     document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5146        if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) {
5147             el.style.display = 'flex';
5148        }
5149    });
5150    
5151    // Show logout button in edit controls when in edit mode
5152    const logoutButton = document.getElementById('logoutButton');
5153    if (logoutButton) {
5154      logoutButton.style.display = 'flex';
5155      logoutButton.classList.add('visible');
5156    }
5157  } else {
5158     // Ensure edit controls container is hidden if not in edit mode
5159     const editControls = document.querySelector('.edit-controls');
5160     if (editControls) {
5161        editControls.classList.remove('visible');
5162        editControls.style.display = 'none';
5163     }
5164     
5165     // Hide logout button when not in edit mode
5166     const logoutButton = document.getElementById('logoutButton');
5167     if (logoutButton) {
5168       logoutButton.style.display = 'none';
5169       logoutButton.classList.remove('visible');
5170     }
5171  }
5172
5173  console.log('Admin/Auth classes set. CSS should handle button visibility.');
5174}
5175
5176// UPDATED: Reset auth UI with class-based approach  
5177function resetAuthUI() {
5178  document.body.classList.remove('hydra-authenticated');
5179  document.body.classList.remove('hydra-admin');
5180  
5181  
5182  const logoutButton = document.getElementById('logoutButton');
5183  if (logoutButton) {
5184    logoutButton.style.display = 'none';
5185    logoutButton.classList.remove('visible');
5186  }
5187  
5188  const editButton = document.getElementById('editButton');
5189  if (editButton) {
5190    editButton.style.display = 'none';
5191    editButton.classList.remove('visible');
5192  }
5193  
5194  // Remove admin status from localStorage
5195  localStorage.removeItem('hydra_is_admin');
5196  localStorage.removeItem('hydra_auth_token');
5197  
5198  if (isEditing) {
5199    toggleEditMode(); // Exit edit mode if active
5200  }
5201}
5202
5203// Debug function - add to your code during testing
5204function debugUIClasses() {
5205  console.log('Current body classes:', document.body.className);
5206  console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized'));
5207  console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated'));
5208  console.log('hydra-admin:', document.body.classList.contains('hydra-admin'));
5209  console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active'));
5210}
5211
5212
5213
5214async function logout() {
5215  console.log('Logout initiated');
5216  
5217  try {
5218    // Show loading indication (optional)
5219    showLoadingOverlay('Logging out...');
5220    
5221    // 1. Properly logout from Magic SDK
5222    if (window.magic) {
5223      try {
5224        await window.magic.user.logout();
5225        console.log('Magic SDK logout successful');
5226      } catch (magicError) {
5227        console.error('Error during Magic logout:', magicError);
5228        // Continue with other logout steps even if Magic SDK logout fails
5229      }
5230    }
5231    
5232    // 2. Reset all authentication state variables
5233    clearHydraClientAuthStorage();
5234    
5235    // 3. Reset all UI state - remove authentication classes from body
5236    document.body.classList.remove('hydra-authenticated');
5237    document.body.classList.remove('hydra-admin');
5238    
5239    // 5. If in edit mode, exit it first
5240    if (document.body.classList.contains('edit-mode-active')) {
5241      console.log('Exiting edit mode before logout');
5242      // If toggleEditMode exists, call it to exit edit mode
5243      if (typeof toggleEditMode === 'function') {
5244        try {
5245          toggleEditMode();
5246        } catch (editError) {
5247          console.error('Error exiting edit mode:', editError);
5248        }
5249      }
5250      
5251      // Ensure edit mode class is removed regardless
5252      document.body.classList.remove('edit-mode-active');
5253    }
5254    
5255    // 6. Ensure all edit UI elements are hidden
5256    
5257    // Hide edit and logout buttons
5258    const editButton = document.getElementById('editButton');
5259    if (editButton) {
5260      editButton.classList.remove('visible');
5261      editButton.style.display = 'none';
5262    }
5263    
5264    const logoutButton = document.getElementById('logoutButton');
5265    if (logoutButton) {
5266      logoutButton.classList.remove('visible');
5267      logoutButton.style.display = 'none';
5268    }
5269    
5270    // Hide edit controls
5271    const editControls = document.querySelector('.edit-controls');
5272    if (editControls) {
5273      editControls.style.display = 'none';
5274      editControls.classList.remove('visible');
5275    }
5276    
5277    // Hide sidebar header if it should be hidden when logged out
5278    const sidebarHeader = document.querySelector('.sidebar-header');
5279    if (sidebarHeader) {
5280      sidebarHeader.style.display = 'none';
5281    }
5282    
5283    // Hide all element controls
5284    document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5285      el.style.display = 'none';
5286    });
5287    
5288    // 7. Reset editor state if applicable
5289    if (window.Parameters) {
5290      window.Parameters.isInEditor = false;
5291    }
5292    
5293    // Hide any editing forms that might be open
5294    const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm');
5295    forms.forEach(form => {
5296      form.style.display = 'none';
5297      if (form.classList.contains('visible')) {
5298        form.classList.remove('visible');
5299      }
5300    });
5301    
5302    // Remove editing class from sidebar
5303    const sidebar = document.querySelector('.sidebar');
5304    if (sidebar) {
5305      sidebar.classList.remove('editing');
5306    }
5307    
5308    // 8. Clear any page-specific state
5309    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
5310      window.PageManager.clearCurrentPage();
5311    }
5312    
5313    // 9. Notify any components that need to know about logout
5314    document.dispatchEvent(new CustomEvent('user-logout', {
5315      detail: { timestamp: Date.now() }
5316    }));
5317    
5318    // 10. Show success message
5319    hideLoadingOverlay();
5320    showSuccessMessage('Successfully logged out');
5321    
5322    console.log('Logout complete');
5323    
5324    // 11. Optional: Refresh the page after a short delay for a clean state
5325    // Uncomment the following lines if you want the page to refresh after logout
5326    /*
5327    setTimeout(() => {
5328      window.location.reload();
5329    }, 1500);
5330    */
5331    
5332  } catch (error) {
5333    console.error('Error during logout process:', error);
5334    hideLoadingOverlay();
5335    showErrorMessage('Error during logout. Please refresh the page.');
5336  }
5337}
5338
5339// Ensure the logout button is properly connected to the logout function
5340document.addEventListener('DOMContentLoaded', function() {
5341  const logoutButton = document.getElementById('logoutButton');
5342  if (logoutButton) {
5343    // Remove any existing event listeners to avoid duplicates
5344    const newButton = logoutButton.cloneNode(true);
5345    logoutButton.parentNode.replaceChild(newButton, logoutButton);
5346    
5347    // Add fresh event listener
5348    newButton.addEventListener('click', function(e) {
5349      e.preventDefault();
5350      e.stopPropagation();
5351      logout();
5352    });
5353  }
5354});
5355
5356function stopAutoAdvanceTimer() {
5357  if (window.currentAutoAdvanceTimerId) {
5358    clearTimeout(window.currentAutoAdvanceTimerId);
5359    window.currentAutoAdvanceTimerId = null; 
5360    console.log('Auto-advance timer stopped.');
5361  }
5362}
5363window.stopAutoAdvanceTimer = stopAutoAdvanceTimer;
5364
5365function beginHydraContentLoad(type, itemId, pageId) {
5366  const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1;
5367  window.__hydraContentLoadGeneration = generation;
5368  window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1;
5369  window.__hydraContentLoadOwner = {
5370    generation,
5371    type,
5372    itemId: itemId === undefined || itemId === null ? '' : String(itemId),
5373    pageId: pageId === undefined || pageId === null ? '' : String(pageId)
5374  };
5375  return generation;
5376}
5377
5378function isHydraContentLoadCurrent(generation, type, itemId) {
5379  if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) {
5380    return false;
5381  }
5382  if (type && window._activeContentType && window._activeContentType !== type) {
5383    return false;
5384  }
5385  if (itemId != null && itemId !== '' && window.activeGalleryId != null &&
5386      String(window.activeGalleryId) !== String(itemId)) {
5387    return false;
5388  }
5389  return true;
5390}
5391
5392window.beginHydraContentLoad = beginHydraContentLoad;
5393window.isHydraContentLoadCurrent = isHydraContentLoadCurrent;
5394
5395function dismissGalleryPasswordOverlay() {
5396  if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') {
5397    window.PageManager.removeGalleryPasswordOverlay();
5398    return;
5399  }
5400  document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) {
5401    if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') {
5402      overlay._hydraPasswordAbort.abort();
5403    }
5404    overlay.remove();
5405  });
5406}
5407window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay;
5408
5409function toggleEditMode() {
5410  // Prevent multiple simultaneous calls
5411  if (window._editModeToggleInProgress) {
5412    console.log('Edit mode toggle already in progress, skipping');
5413    return;
5414  }
5415  window._editModeToggleInProgress = true;
5416
5417  console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined');
5418
5419  stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance
5420
5421  // Authentication and Authorization Checks
5422  if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) {
5423    alert('You must be logged in to edit');
5424    window._editModeToggleInProgress = false;
5425    return;
5426  }
5427  if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) {
5428    window.isAdmin = true; // Synchronize global flag
5429  }
5430  if (!window.isAdmin) {
5431    alert('You must be an admin to edit');
5432    window._editModeToggleInProgress = false;
5433    return;
5434  }
5435
5436  const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false);
5437  const newEditState = !currentEditState;
5438  
5439  console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`);
5440
5441  // Apply body class for edit mode styling
5442  if (newEditState) {
5443    console.log('Entering edit mode - setting body class and theme');
5444    document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode
5445    document.body.classList.add('edit-mode-active');
5446  } else {
5447    console.log('Exiting edit mode - removing body class and theme');
5448    document.body.removeAttribute('data-theme');
5449    document.body.classList.remove('edit-mode-active');
5450  }
5451
5452  // Update the global state and dispatch event
5453  if (typeof window.updateGlobalEditState === 'function') {
5454    window.updateGlobalEditState(newEditState); 
5455  } else {
5456    window.isEditing = newEditState; // Fallback
5457    document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } }));
5458    if (!newEditState && typeof window.closeOptionsPanel === 'function') {
5459      window.closeOptionsPanel();
5460    }
5461  }
5462
5463  if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') {
5464    setTimeout(syncContentMarginToHorizontalHeader, 50);
5465    setTimeout(syncContentMarginToHorizontalHeader, 350);
5466  }
5467
5468  const sidebarEditControls = document.querySelector('.edit-controls');
5469
5470  if (newEditState) {
5471    // Entering edit mode
5472    if (sidebarEditControls) {
5473        sidebarEditControls.style.display = 'flex';
5474        sidebarEditControls.classList.add('visible');
5475    }
5476    
5477    // Show logout button in edit controls when entering edit mode
5478    const logoutButton = document.getElementById('logoutButton');
5479    if (logoutButton) {
5480      logoutButton.style.display = 'flex';
5481      logoutButton.classList.add('visible');
5482    }
5483    
5484    // Always render the tree-like menu in the sidebar for editing
5485    if (typeof window.renderGalleries === 'function') {
5486        console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.');
5487        window.renderGalleries(); // This will show all items, including those with visible:false
5488    }
5489
5490    setTimeout(() => {
5491        if (typeof window.initializeNestedSortables === 'function') {
5492            console.log('Initializing nested sortables for edit mode');
5493            window.initializeNestedSortables();
5494        }
5495        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
5496            window.SidebarManager._reinitializeNestedSortables();
5497        }
5498    }, 150);
5499
5500    // If the currently active item was invisible, reload it now that we are in edit mode.
5501    if(window.galleries && window.activeGalleryId){
5502        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5503        if (currentGalleryItem && currentGalleryItem.visible === false) {
5504            console.log('toggleEditMode (Edit): Active item was invisible, reloading it.');
5505            if (currentGalleryItem.isPage && window.loadPage) {
5506                window.loadPage(window.activeGalleryId);
5507            } else if(window.loadGallery) {
5508                window.loadGallery(window.activeGalleryId);
5509            }
5510        }
5511    }
5512    if(typeof window.showStyleEditor === 'function' &&
5513        !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) {
5514      window.showStyleEditor(false); // Ensure style editor is hidden initially
5515    }
5516
5517  } else {
5518    // Exiting edit mode (going to Live view)
5519    if (sidebarEditControls) {
5520        sidebarEditControls.style.display = 'none';
5521        sidebarEditControls.classList.remove('visible');
5522    }
5523    
5524    // Hide logout button when exiting edit mode
5525    const logoutButton = document.getElementById('logoutButton');
5526    if (logoutButton) {
5527      logoutButton.style.display = 'none';
5528      logoutButton.classList.remove('visible');
5529    }
5530    if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') {
5531      window.MenuStyleCustomizer._prepareOverlayForLiveView();
5532    }
5533    if(typeof window.closeAllForms === 'function') window.closeAllForms();
5534    
5535    if (typeof window.destroyNestedSortables === 'function') {
5536        window.destroyNestedSortables();
5537    } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5538        window.sortableInstances.forEach(instance => instance.destroy());
5539        window.sortableInstances = [];
5540    }
5541
5542    // Re-render the menu based on the actual view mode layout.
5543    // These functions should internally handle not showing items with visible:false.
5544    let currentLayout = 'sidebar';
5545    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5546        currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5547    }
5548    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
5549        window.renderHorizontalMenu();
5550    } else if (typeof window.renderGalleries === 'function') {
5551        window.renderGalleries();
5552    }
5553
5554    // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view
5555    if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') {
5556      window.SidebarManager._syncWithGlobalEditState();
5557      window.SidebarManager.renderElements();
5558    }
5559
5560    // Handle the currently active gallery/page.
5561    if (window.galleries && window.activeGalleryId) {
5562        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5563        if (currentGalleryItem) {
5564            // The content for currentGalleryItem should remain loaded,
5565            // even if currentGalleryItem.visible is false.
5566            // The menu rendering above will hide it from the list if it's invisible.
5567            
5568            // Apply page-specific menu hiding rules.
5569            const bodyEl = document.body;
5570            // When exiting edit mode, isInEditMode() will effectively be false for this check.
5571            if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 
5572                bodyEl.classList.add('menu-hidden-on-page');
5573            } else {
5574                bodyEl.classList.remove('menu-hidden-on-page');
5575            }
5576            applyMenuOverlayClassForGallery(currentGalleryItem);
5577            // Ensure active states in the (potentially now filtered) menu are updated.
5578            // If the active item is invisible, it won't be marked active in the menu, which is fine.
5579            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5580            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5581
5582        } else {
5583            // activeGalleryId points to a non-existent item, so clear it and the content.
5584            console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.');
5585            window.activeGalleryId = null;
5586            const galleryContainer = document.querySelector('.gallery-container');
5587            if (galleryContainer) galleryContainer.innerHTML = '';
5588            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5589            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5590        }
5591    }
5592    
5593    // If, after all that, no content is effectively active (e.g., initial load to root, 
5594    // or activeGalleryId became null because the item was deleted)
5595    // then try to load a default page (home page or first visible gallery).
5596    const galleryContainer = document.querySelector('.gallery-container');
5597    // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted)
5598    const noActiveContent = window.activeGalleryId === null || 
5599                            (galleryContainer && galleryContainer.innerHTML.trim() === '');
5600
5601    if (noActiveContent && window.galleries) {
5602      console.log("toggleEditMode (Live): No active content, attempting to load default page.");
5603      const homePage = window.galleries.find(g => g.isHomePage === true);
5604      let loadedDefault = false;
5605      if (homePage) {
5606          // Load home page regardless of its visibility status for this default loading logic
5607          console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`);
5608          if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; }
5609          else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; }
5610      }
5611      
5612      if (!loadedDefault) {
5613          // If no home page, load the first *visible* gallery/page in live view.
5614          const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
5615          if (firstVisibleGallery) {
5616            console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`);
5617            if (firstVisibleGallery.isPage && window.loadPage) {
5618                window.loadPage(firstVisibleGallery.id);
5619            } else if(window.loadGallery) {
5620                window.loadGallery(firstVisibleGallery.id);
5621            }
5622          } else {
5623            console.log('toggleEditMode (Live): No home page and no visible items to load.');
5624          }
5625      }
5626    }
5627
5628    if (window.MenuStyleCustomizer &&
5629        window.MenuStyleCustomizer._overlayOpen &&
5630        typeof window.MenuStyleCustomizer.applyStyles === 'function') {
5631      window.MenuStyleCustomizer.applyStyles();
5632    }
5633  }
5634  window._editModeToggleInProgress = false;
5635}
5636
5637
5638
5639/**
5640 * Improved toggleSidebarElementForm - Closes other forms first
5641 */
5642function toggleSidebarElementForm() {
5643  const formId = 'sidebarElementForm';
5644  const form = document.getElementById(formId);
5645  
5646  if (!form) return;
5647  
5648  // If this form is already open, just close everything
5649  if (window.currentOpenForm === formId) {
5650    closeAllForms();
5651    return;
5652  }
5653  
5654  // Otherwise close all forms and open this one
5655  closeAllForms();
5656  
5657  // Show the form
5658  form.style.display = 'block';
5659  form.classList.add('visible');
5660  
5661  // Set this as the current open form
5662  window.currentOpenForm = formId;
5663}
5664
5665// New function to add a sidebar element
5666function addSidebarElement() {
5667const typeInput = document.querySelector('input[name="sidebarElementType"]:checked');
5668if (!typeInput) {
5669alert('Please select an element type');
5670return;
5671}
5672
5673const type = typeInput.value;
5674
5675// Use the SidebarManager to add the element
5676if (window.SidebarManager) {
5677const newElementId = window.SidebarManager.addElement(type);
5678toggleSidebarElementForm();
5679
5680if (newElementId) {     
5681  // Save the changes to KV store
5682  saveGalleries().then(() => {
5683    console.log(`New ${type} element added successfully with ID: ${newElementId}`);
5684  }).catch(error => {
5685    console.error('Error saving new element:', error);
5686  });
5687}
5688} else {
5689alert('SidebarManager not available');
5690}
5691}
5692
5693
5694// Global variable to store metadata
5695window.siteMetadata = {
5696  title: '',
5697  description: '',
5698  googleAnalytics: '',
5699  noIndex: false,
5700  favicon: '',
5701  ogImageUrl: '',
5702  contentLanguage: 'en',  // Default to English
5703  copyrightEnabled: false,
5704  copyrightText: '',
5705  customPublicSiteEnabled: false,
5706  customPublicSiteHtmlR2Key: '',
5707  customPublicSiteHtmlFilename: '',
5708  siteWideBasicAuth: undefined
5709};
5710
5711// Global variable to store uploaded favicon URL temporarily
5712window.uploadedFaviconUrl = '';
5713
5714function updateMetadataCopyrightFieldState() {
5715  const toggle = document.getElementById('metadataCopyrightEnabled');
5716  const textInput = document.getElementById('metadataCopyrightText');
5717  const isEnabled = !!(toggle && toggle.checked);
5718
5719  if (textInput) {
5720    textInput.disabled = !isEnabled;
5721  }
5722
5723  // Intentionally leave the surrounding form group in place; the disabled attribute on the input
5724  // provides the visual cue in PicoCSS.
5725}
5726
5727/**
5728 * Improved toggleMetadataEditor - Closes other forms first
5729 */
5730function toggleMetadataEditor() {
5731  const formId = 'metadataEditor';
5732  const editor = document.getElementById(formId);
5733  
5734  if (!editor) return;
5735  
5736  // If this form is already open, just close everything
5737  if (window.currentOpenForm === formId) {
5738    closeAllForms();
5739    return;
5740  }
5741  
5742  // Otherwise close all forms and open this one
5743  closeAllForms();
5744  
5745  // Load current values before showing
5746  loadMetadataValues();
5747  editor.style.display = 'block';
5748  
5749  // Set this as the current open form
5750  window.currentOpenForm = formId;
5751  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
5752    syncCustomPublicSiteBypassVisibility();
5753  }
5754}
5755
5756
5757// Load metadata values into the form
5758function loadMetadataValues() {
5759  // Get the current values from the global object
5760  const metadata = window.siteMetadata || {};
5761  console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata));
5762  console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics);
5763  
5764  console.warn('=== FAVICON LOAD DEBUG ===');
5765  console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata));
5766  console.warn('metadata object:', JSON.stringify(metadata));
5767  console.warn('metadata.favicon value:', metadata.favicon);
5768  console.warn('metadata.favicon type:', typeof metadata.favicon);
5769  
5770  // Populate form fields
5771  document.getElementById('metaTitle').value = metadata.title || '';
5772  document.getElementById('metaDescription').value = metadata.description || '';
5773  document.getElementById('googleAnalytics').value = metadata.googleAnalytics || '';
5774  document.getElementById('noIndexToggle').checked = metadata.noIndex || false;
5775
5776  // Schema / Identity fields
5777  const schemaType = metadata.schemaType || '';
5778  const personRadio = document.getElementById('schemaTypePerson');
5779  const orgRadio = document.getElementById('schemaTypeOrganization');
5780  if (personRadio) personRadio.checked = schemaType === 'Person';
5781  if (orgRadio) orgRadio.checked = schemaType === 'Organization';
5782  const jobTitleEl = document.getElementById('schemaJobTitle');
5783  if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || '';
5784  const cityEl = document.getElementById('schemaCity');
5785  if (cityEl) cityEl.value = metadata.schemaCity || '';
5786  const emailEl = document.getElementById('schemaEmail');
5787  if (emailEl) emailEl.value = metadata.schemaEmail || '';
5788  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
5789  if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || '';
5790  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
5791  if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || '';
5792  if (typeof updateSchemaFields === 'function') updateSchemaFields();
5793
5794  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
5795  const copyrightInput = document.getElementById('metadataCopyrightText');
5796  if (copyrightToggle) {
5797    const enabled = !!metadata.copyrightEnabled;
5798    copyrightToggle.checked = enabled;
5799    if (copyrightInput) {
5800      copyrightInput.value = metadata.copyrightText || '';
5801    }
5802  }
5803  updateMetadataCopyrightFieldState();
5804
5805  const customPubToggle = document.getElementById('customPublicSiteEnabled');
5806  if (customPubToggle) {
5807    customPubToggle.checked = !!metadata.customPublicSiteEnabled;
5808  }
5809  const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key');
5810  if (customPubKeyEl) {
5811    customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || '';
5812  }
5813  const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel');
5814  if (customPubFileLabel) {
5815    customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename
5816      ? ('Current file: ' + metadata.customPublicSiteHtmlFilename)
5817      : 'No HTML file uploaded yet.';
5818  }
5819
5820  const swAuth = metadata.siteWideBasicAuth;
5821  const swEnabled = !!(swAuth && swAuth.username && swAuth.password);
5822  const swEnableEl = document.getElementById('siteWideBasicAuthEnabled');
5823  if (swEnableEl) swEnableEl.checked = swEnabled;
5824  const swUserEl = document.getElementById('siteWideBasicAuthUsername');
5825  if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : '';
5826  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
5827  if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : '';
5828  const swRealmEl = document.getElementById('siteWideBasicAuthRealm');
5829  if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : '';
5830  if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState();
5831  
5832  // Set content language dropdown (defaults to 'en')
5833  const contentLanguageSelect = document.getElementById('contentLanguage');
5834  if (contentLanguageSelect) {
5835    contentLanguageSelect.value = metadata.contentLanguage || 'en';
5836    console.log('Loaded content language:', contentLanguageSelect.value);
5837  }
5838  
5839  const faviconInput = document.getElementById('faviconUrl');
5840  if (faviconInput) {
5841    // Check if we have a recently uploaded favicon URL
5842    const faviconValue = metadata.favicon || window.uploadedFaviconUrl || '';
5843    faviconInput.value = faviconValue;
5844    console.warn('Set favicon input value to:', faviconInput.value);
5845    console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl);
5846  } else {
5847    console.error('Favicon input element not found!');
5848  }
5849  
5850  // Show favicon preview if there's an existing favicon
5851  if (metadata.favicon) {
5852    console.warn('Showing favicon preview for:', metadata.favicon);
5853    showFaviconPreview(metadata.favicon);
5854  } else {
5855    console.warn('No favicon to preview');
5856  }
5857
5858  const ogInput = document.getElementById('ogImageUrl');
5859  if (ogInput) {
5860    const ogVal = (metadata.ogImageUrl || '').trim();
5861    ogInput.value = ogVal;
5862  }
5863  const ogBtnText = document.getElementById('uploadOgImageBtnText');
5864  if (ogBtnText) {
5865    ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image';
5866  }
5867  if ((metadata.ogImageUrl || '').trim()) {
5868    showOgImagePreview(metadata.ogImageUrl.trim());
5869  } else {
5870    const ogCont = document.getElementById('ogImagePreviewContainer');
5871    if (ogCont) ogCont.innerHTML = '';
5872  }
5873  
5874  console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value);
5875}
5876
5877// Save metadata values
5878function saveMetadata() {
5879  // Get values from form
5880  const googleAnalyticsElement = document.getElementById('googleAnalytics');
5881  console.log('Google Analytics element found:', !!googleAnalyticsElement);
5882  console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND');
5883  
5884  const faviconInput = document.getElementById('faviconUrl');
5885  const faviconValue = faviconInput ? faviconInput.value : '';
5886  const ogImageInput = document.getElementById('ogImageUrl');
5887  const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : '';
5888  
5889  const contentLanguageSelect = document.getElementById('contentLanguage');
5890  const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en';
5891
5892  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
5893  const copyrightInput = document.getElementById('metadataCopyrightText');
5894  const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false;
5895  const copyrightText = copyrightInput ? copyrightInput.value.trim() : '';
5896  
5897  console.warn('=== FAVICON SAVE DEBUG ===');
5898  console.warn('Favicon input element exists:', !!faviconInput);
5899  console.warn('Favicon input value:', faviconValue);
5900  console.warn('Favicon input value length:', faviconValue.length);
5901  
5902  console.warn('=== CONTENT LANGUAGE SAVE ===');
5903  console.warn('Content language selected:', contentLanguageValue);
5904  
5905  const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : '';
5906  // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope)
5907  let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : '';
5908  if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) {
5909    const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i);
5910    gaId = match ? match[1] : '';
5911  }
5912  const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key');
5913  const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore');
5914  const schemaPersonRadio = document.getElementById('schemaTypePerson');
5915  const schemaOrgRadio = document.getElementById('schemaTypeOrganization');
5916  const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person'
5917    : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization'
5918    : '';
5919  const schemaJobTitleEl = document.getElementById('schemaJobTitle');
5920  const schemaCityEl = document.getElementById('schemaCity');
5921  const schemaEmailEl = document.getElementById('schemaEmail');
5922  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
5923  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
5924  const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : '';
5925  const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : '';
5926
5927  const normalizePublicAbsoluteUrl = (value) => {
5928    if (!value) return '';
5929    try {
5930      const parsed = new URL(value);
5931      if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return '';
5932      return parsed.href;
5933    } catch {
5934      return '';
5935    }
5936  };
5937  const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw);
5938  const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw);
5939  if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) {
5940    alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.');
5941    return;
5942  }
5943
5944  const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked);
5945  const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : '';
5946  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
5947  const swPass = swPassEl ? swPassEl.value : '';
5948  const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : '';
5949
5950  if (siteWideBasicAuthEnabled && (!swUser || !swPass)) {
5951    alert('Site-wide password: enter both username and password, or turn off the option.');
5952    return;
5953  }
5954
5955  const metadata = {
5956    title: document.getElementById('metaTitle').value,
5957    description: document.getElementById('metaDescription').value,
5958    googleAnalytics: gaId,
5959    noIndex: document.getElementById('noIndexToggle').checked,
5960    favicon: faviconValue,
5961    ogImageUrl: ogImageValue,
5962    contentLanguage: contentLanguageValue,
5963    copyrightEnabled,
5964    copyrightText,
5965    customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked),
5966    customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '',
5967    customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '',
5968    schemaType: schemaType || undefined,
5969    schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '',
5970    schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
5971    schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '',
5972    ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {})
5973  };
5974
5975  if (siteWideBasicAuthEnabled && swUser && swPass) {
5976    metadata.siteWideBasicAuth = {
5977      username: swUser,
5978      password: swPass,
5979      ...(swRealm ? { realm: swRealm } : {})
5980    };
5981  }
5982  
5983  console.warn('Metadata object being created:', JSON.stringify(metadata));
5984  console.warn('Favicon in metadata object:', metadata.favicon);
5985
5986  // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet)
5987  if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) {
5988    googleAnalyticsElement.value = metadata.googleAnalytics;
5989  }
5990
5991  // Update global metadata object immediately
5992  window.siteMetadata = metadata;
5993  console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata));
5994  if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
5995    window.SidebarManager.updateMetadataFooter();
5996  }
5997
5998  // Ensure we have the latest galleries, sidebar elements, and styles
5999  // before constructing the customData object.
6000
6001  // Synchronize galleries first if the function exists
6002  if (typeof synchronizeGalleries === 'function') {
6003      synchronizeGalleries();
6004      console.log('Galleries synchronized within saveMetadata');
6005  }
6006
6007  // Retrieve current galleries (use window.galleries as the source of truth after sync)
6008  const currentGalleries = window.galleries || galleries || [];
6009  console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`);
6010
6011  // Retrieve current sidebar elements
6012  const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'
6013    ? window.SidebarManager.getElementsForSave()
6014    : null;
6015
6016  // Retrieve current menu styles
6017  const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {};
6018
6019  // Now construct customData using the retrieved current state
6020  if (window.saveGalleries) {
6021    const customData = {
6022      galleries: currentGalleries, // Use the synchronized/retrieved galleries
6023      siteMetadata: metadata, // Use the metadata gathered from the form
6024      menuStyles: currentMenuStyles // Use the retrieved menu styles
6025    };
6026    if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) {
6027      customData.sidebarElements = currentSidebarElements;
6028    }
6029  
6030
6031    console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`);
6032    console.log("Metadata being saved:", JSON.stringify(metadata));
6033    console.log("Google Analytics specifically:", metadata.googleAnalytics);
6034    console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)');
6035    console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles));
6036
6037    // Save with our correctly constructed custom data object
6038    window.saveGalleries(customData)
6039      .then(() => {
6040        alert('Metadata saved successfully');
6041        toggleMetadataEditor(); // Hide the editor
6042      })
6043      .catch(error => {
6044        console.error('Error saving metadata:', error);
6045        alert('Error saving metadata. Please try again.');
6046      });
6047  } else {
6048    alert('Save function not available');
6049  }
6050}
6051
6052  
6053// Function to add new entries to the style editor
6054function showStyleEditor(display = false) {
6055  if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') {
6056    window.MenuStyleCustomizer.openOverlay();
6057    window.currentOpenForm = 'menuStyleEditor';
6058    return;
6059  }
6060  hideStyleEditor();
6061}
6062
6063// New function to hide the style editor
6064function hideStyleEditor() {
6065  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6066    window.MenuStyleCustomizer.closeOverlay();
6067  }
6068  const styleEditor = document.getElementById('menuStyleEditor');
6069  if (styleEditor) {
6070    styleEditor.style.display = 'none';
6071  }
6072}
6073
6074
6075/**
6076 * Improved toggleAddForm - Closes other forms first and ensures proper sizing
6077 */
6078function toggleAddForm() {
6079  const formId = 'addForm';
6080  const form = document.getElementById(formId);
6081  
6082  if (!form) return;
6083  
6084  // If this form is already open, just close everything
6085  if (window.currentOpenForm === formId) {
6086    closeAllForms();
6087    return;
6088  }
6089  
6090  // Otherwise close all forms and open this one
6091  closeAllForms();
6092  
6093  // Determine the appropriate max-height based on content
6094  // First check if we're showing the page form by checking the radio button
6095  const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked;
6096  
6097  // Allow extra space for the page form
6098  const maxHeight = isPageSelected ? '600px' : '500px';
6099  
6100  // Show the form with proper max height
6101  form.style.maxHeight = maxHeight;
6102  form.style.overflow = 'visible';
6102 // Ensure all content is visible
6103  form.classList.add('visible');
6104  
6105  // Also add inline styles to ensure visibility during animation
6106  form.style.padding = '15px';
6107  
6108  // Force recalculation of max-height after UI is visible
6109  setTimeout(() => {
6110    // Get actual content height + padding
6111    const contentHeight = form.scrollHeight + 30; // Add padding
6112    
6113    // Set max-height based on content with minimum of 500px
6114    form.style.maxHeight = Math.max(contentHeight, 500) + 'px';
6115    
6116    console.log(`Setting add form max height to ${form.style.maxHeight}`);
6117  }, 50);
6118  
6119  // Populate parent options
6120  const parentSelect = document.getElementById('galleryParent');
6121  if (parentSelect) {
6122    parentSelect.innerHTML = '<option value="">None (Top Level)</option>';
6123    
6124    // Add all galleries that could be parents (including submenus)
6125    galleries.forEach(gallery => {
6126      parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
6127    });
6128  }
6129  
6130  // Set this as the current open form
6131  window.currentOpenForm = formId;
6132}
6133
6134
6135// Store the currently open form ID
6136window.currentOpenForm = null;
6137
6138/**
6139 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer).
6140 * Returns true when a child was closed so callers can keep the Style Customizer open.
6141 */
6142function closeTopmostStyleCustomizerChildOverlay() {
6143  if (document.querySelector('.pe-palette-explorer-overlay')) {
6144    if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') {
6145      const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay');
6146      window.PaletteExplorer.close();
6147      if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) {
6148        return false;
6149      }
6150      return wasOpen;
6151    }
6152  }
6153  if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') &&
6154      window.MenuStyleCustomizer &&
6155      typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') {
6156    window.MenuStyleCustomizer.closeStyleHistoryOverlay();
6157    return true;
6158  }
6159  if (document.querySelector('.pe-font-explorer-overlay') &&
6160      window.MenuStyleCustomizer &&
6161      typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') {
6162    window.MenuStyleCustomizer.closeFontExplorer();
6163    return true;
6164  }
6165  if (document.querySelector('.pe-custom-code-overlay') &&
6166      window.MenuStyleCustomizer &&
6167      typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') {
6168    window.MenuStyleCustomizer.closeCustomCodeOverlay();
6169    return true;
6170  }
6171  const colorMixer = document.getElementById('colorMixerPopup');
6172  if (colorMixer && colorMixer.classList.contains('is-open')) {
6173    colorMixer.classList.remove('is-open');
6174    return true;
6175  }
6176  return false;
6177}
6178
6179/**
6180 * Close all editor forms with improved handling for add form
6181 */
6182function closeAllForms() {
6183  if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') {
6184    while (closeTopmostStyleCustomizerChildOverlay()) {
6185      // Close nested Style Customizer panels (palette, custom code, color mixer) first.
6186    }
6187  }
6188
6189  const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer &&
6190    window.MenuStyleCustomizer._overlayOpen &&
6191    window.MenuStyleCustomizer._overlayMinimized);
6192
6193  if (!keepMinimizedStyleCustomizer &&
6194      window.MenuStyleCustomizer &&
6195      typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6196    const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen;
6197    window.MenuStyleCustomizer.closeOverlay();
6198    if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) {
6199      return;
6200    }
6201  }
6202
6203  // Add Form - Special handling to ensure proper animation
6204  const addForm = document.getElementById('addForm');
6205  if (addForm) {
6206    addForm.classList.remove('visible');
6207    
6208    // Also force style reset after animation completes
6209    setTimeout(() => {
6210      if (!addForm.classList.contains('visible')) {
6211        addForm.style.maxHeight = '0';
6212        addForm.style.overflow = 'hidden';
6213        addForm.style.padding = '0 15px';
6214      }
6215    }, 350); // Set slightly longer than CSS transition
6216  }
6217  
6218  // Style Editor (do not hide the overlay host while the minimized mutate bar is live)
6219  const styleEditor = document.getElementById('menuStyleEditor');
6220  if (styleEditor && !keepMinimizedStyleCustomizer) {
6221    styleEditor.style.display = 'none';
6222  }
6223  
6224  // Metadata Editor
6225  const metadataEditor = document.getElementById('metadataEditor');
6226  if (metadataEditor) {
6227    metadataEditor.style.display = 'none';
6228  }
6229  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
6230    syncCustomPublicSiteBypassVisibility();
6231  }
6232  
6233  // Sidebar Element Form
6234  const sidebarElementForm = document.getElementById('sidebarElementForm');
6235  if (sidebarElementForm) {
6236    sidebarElementForm.style.display = 'none';
6237    sidebarElementForm.classList.remove('visible');
6238  }
6239  
6240  // Import Classic Form
6241  const importClassicForm = document.getElementById('importClassicForm');
6242  if (importClassicForm) {
6243    importClassicForm.style.display = 'none';
6244    importClassicForm.classList.remove('visible');
6245  }
6246  
6247  // Clear the current open form
6248  window.currentOpenForm = null;
6249}
6250function downloadJson() {
6251  const data = JSON.stringify(galleries, null, 2);
6252  const blob = new Blob([data], { type: 'application/json' });
6253  const url = URL.createObjectURL(blob);
6254  const a = document.createElement('a');
6255  a.href = url;
6256  a.download = 'galleries.json';
6257  document.body.appendChild(a);
6258  a.click();
6259  document.body.removeChild(a);
6260  URL.revokeObjectURL(url);
6261}
6262
6263let galleriesBackup = [];
6264
6265
6266
6267/**
6268 * Improved version of initializeNestedSortables with a better nesting approach
6269 */
6270function initializeNestedSortables() {
6271  console.log('Initializing nested sortables with improved nesting logic...');
6272
6273  // Destroy leftover instances before binding again. A second Sortable on the
6274  // same list makes items preview during drag and snap back on release.
6275  document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => {
6276    try {
6277      const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null;
6278      if (existing && typeof existing.destroy === 'function') existing.destroy();
6279    } catch (error) {
6280      console.warn('Could not destroy existing nested sortable:', error);
6281    }
6282  });
6283
6284  // Create a backup of galleries
6285  try {
6286    galleriesBackup = JSON.parse(JSON.stringify(galleries));
6287  } catch (error) {
6288    console.error('Error creating galleries backup:', error);
6289    galleriesBackup = [];
6290  }
6291
6292  // Find all nested sortable lists
6293  const nestedSortables = document.querySelectorAll('.nested-sortable');
6294  if (nestedSortables.length === 0) {
6295    console.warn('No nested sortable elements found! Check your HTML 
6295structure.');
6296    return;
6297  }
6298
6299  // Helper: compute depth based on ancestor nested lists
6300  function computeDepth(liEl) {
6301    let depth = 0;
6302    let node = liEl;
6303    while (node) {
6304      const parentOl = node.closest('ol.nested-sortable');
6305      if (!parentOl) break;
6306      const parentLi = parentOl.closest('li');
6307      if (parentLi) {
6308        depth += 1;
6309        node = parentLi;
6310      } else {
6311        break;
6312      }
6313    }
6314    // Top-level depth should be 0
6315    return Math.max(depth - 1, 0);
6316  }
6317
6318  // Helper: apply indentation styling to an li and its subtree
6319  function applyIndentationFromDepth(rootLi) {
6320    if (!rootLi) return;
6321    const applyToItem = (li) => {
6322      const depth = computeDepth(li);
6323      // Update data-nesting-level attribute
6324      li.setAttribute('data-nesting-level', depth);
6325      // Apply padding-left style
6326      const content = li.querySelector('.gallery-item-content');
6327      if (content) {
6328        content.style.paddingLeft = (depth * 16) + 'px';
6329      }
6330    };
6331    applyToItem(rootLi);
6332    rootLi.querySelectorAll('li').forEach(applyToItem);
6333  }
6334
6335  // Helper: ensure parent has proper state and nested list
6336  function ensureParentChildState(parentLi) {
6337    if (!parentLi) return null;
6338    parentLi.classList.add('has-children');
6339    let nestedList = parentLi.querySelector('ol.nested-sortable');
6340    if (!nestedList) {
6341      nestedList = document.createElement('ol');
6342      nestedList.className = 'nested-sortable';
6343      parentLi.appendChild(nestedList);
6344    }
6345    return nestedList;
6346  }
6347
6348  // Helper: clean parent if it has no children
6349  function cleanupParentIfEmpty(parentLi) {
6350    if (!parentLi) return;
6351    const nestedList = parentLi.querySelector(':scope > ol.nested-sortable');
6352    const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10);
6353    const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null;
6354    // Opted-in pages/galleries must keep an empty drop zone and chevron.
6355    if (isNavigableMenuParent(parentItem)) {
6356      parentLi.classList.add('has-children');
6357      return;
6358    }
6359    if (nestedList && nestedList.children.length === 0) {
6360      nestedList.remove();
6361      parentLi.classList.remove('has-children');
6362    }
6363  }
6364
6365  // Initialize each sortable with improved nesting settings
6366  let sortableInstances = [];
6367
6368  for (let i = 0; i < nestedSortables.length; i++) {
6369    try {
6370      const container = nestedSortables[i];
6371      
6372      // Track where the placeholder indicates the item should be placed
6373      // This helps us detect when SortableJS incorrectly auto-nests based on mouse position
6374      let intendedPlacementContainer = null;
6375      let intendedRelatedElement = null;
6376      
6377      // Track last move to prevent oscillation
6378      let lastMoveTime = 0;
6379      let lastMoveRelatedId = null;
6380      const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds)
6381      
6382      // Track if we're dragging the last item in a folder (to prevent boundary oscillation)
6383      let isLastItemInFolder = false;
6384      let draggedItemOriginalContainer = null;
6385      
6386      // Track container changes to prevent rapid switching (hysteresis)
6387      let firstPositionFixLoopActive = false; // Prevent multiple fix loops
6388      let lastContainerDecision = null; // 'inside' or 'outside'
6389      let lastContainerDecisionTime = 0;
6390      const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches
6391      
6392      const sortable = new Sortable(container, {
6393        group: 'nested',
6394        animation: 150, // Reduced animation during drag to reduce lag
6395        easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function
6396        fallbackOnBody: false, // Disable forced fallback on desktop for better performance
6397        forceFallback: false, // Use native drag on desktop
6398        
6399        // CRITICAL: These settings make nesting much easier
6400        // Higher swap threshold makes it easier to drag over items
6401        swapThreshold: 0.65, // Increased to reduce oscillation within same folder
6402        
6403        // IMPROVED NESTING SETTINGS
6404        // Used when determining if an item should be nested
6405        invertSwap: false,   // Disable to reduce oscillation
6406        direction: 'vertical', // Lock direction to vertical to reduce oscillation
6407        
6408        // Use handle for dragging
6409        handle: '.nested-sortable-handle',
6410        
6411        // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it
6412        // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both:
6413        // - Quick click → toggles folder (onclick handler fires before drag starts)
6414        // - Click and drag → drags the item (SortableJS handles the drag)
6415        // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true,
6416        // the onclick should fire quickly enough before SortableJS intercepts
6417        filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon',
6418        preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only)
6419        
6420        // Distinguish between click and drag
6421        delay: 120,
6422        delayOnTouchOnly: true,
6423        
6424        // Empty container settings
6425        emptyInsertThreshold: 10,
6426        
6427        // Add custom ghost class
6428        ghostClass: 'sortable-ghost',
6429        chosenClass: 'sortable-chosen',
6430        dragClass: 'sortable-drag',
6431        
6432        // Make sure we listen to external move events
6433        dragoverBubble: true,
6434        
6435        // Better drag preview stability
6436        scroll: true,
6437        scrollSensitivity: 100,
6438        scrollSpeed: 20,
6439        
6440        // Reduce oscillation by making placeholder movement less sensitive
6441        forceFallback: false, // Use native drag for better performance
6442        fallbackTolerance: 0, // No tolerance - use exact position
6443        
6444        /**
6445         * NESTING LOGIC: Prevent auto-nesting based on hover
6446         * Instead, rely on SortableJS's placeholder position which is accurate
6447         * We'll verify and correct placement in onEnd based on actual DOM position
6448         */
6449        onMove: function(evt, originalEvent) {
6450          const dragged = evt.dragged;
6451          const related = evt.related;
6452          
6453          if (!dragged || !related) return true;
6454          
6455          // Safety check: Ensure items have data-id attributes
6456          // This can be missing if item was just added and DOM not fully updated
6457          if (!dragged.dataset.id || !related.dataset.id) {
6458            console.warn('⚠️ [Drag] Item missing data-id attribute, aborting drag', {
6459              draggedHasId: !!dragged.dataset.id,
6460              relatedHasId: !!related.dataset.id
6461            });
6462            return false;
6463          }
6464          
6465          // Get the IDs to check for circular references
6466          const draggedId = parseInt(dragged.dataset.id);
6467          const relatedId = parseInt(related.dataset.id);
6468          
6469          // Don't allow dropping inside itself or its descendants
6470          if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) {
6471            return false;
6472          }
6473          
6474          const currentTime = Date.now();
6475          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
6476          const draggedParent = draggedItemOriginalContainer || dragged.parentElement;
6477          
6478          // Determine if we're moving inside or outside a folder
6479          // IMPORTANT: Check if related element IS the folder item itself (parent li)
6480          // When targeting first position, SortableJS might use the folder item as related
6481          const isRelatedFolderItem = related.classList && related.classList.contains('has-children');
6482          
6483          // CRITICAL: Handle empty folder case FIRST - before any other logic
6484          // When dragging into an empty folder, related is the folder item itself
6485          // This is essentially the same as targeting first position, but for an empty folder
6486          // We need to allow this immediately, bypassing all debounce and boundary checks
6487          if (isRelatedFolderItem) {
6488            const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable');
6489            const relatedIsNavigableParent = related.classList.contains('navigable-parent-item');
6490            // Navigable parents look like normal links. Only treat them as an
6491            // empty folder when the pointer is actually over the empty list.
6492            let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0);
6493            if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) {
6494              const listRect = folderNestedSortable.getBoundingClientRect();
6495              const pointerY = originalEvent.clientY;
6496              allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10;
6497            }
6498            if (allowEmptyFolderInsert) {
6499              // Empty folder detected - this is a drop into empty folder (first position)
6500              // Always allow it, bypass all debounce and boundary logic
6501              lastMoveTime = currentTime;
6502              lastMoveRelatedId = relatedId;
6503              intendedPlacementContainer = folderNestedSortable;
6504              intendedRelatedElement = null; // No related element in empty folder
6505              // Mark as moving inside folder for tracking
6506              lastContainerDecision = 'inside';
6507              lastContainerDecisionTime = currentTime;
6508              
6509              // CRITICAL: Manually move placeholder into empty folder's nested-sortable
6510              // SortableJS might not do this automatically for empty containers
6511              requestAnimationFrame(() => {
6512                const placeholder = document.querySelector('.sortable-ghost');
6513                if (placeholder && placeholder.parentElement !== folderNestedSortable) {
6514                  // Move placeholder into the empty folder
6515                  folderNestedSortable.appendChild(placeholder);
6516                }
6517                
6518                // Update ghost indentation for empty folder
6519                const ghost = document.querySelector('.sortable-ghost');
6520                if (ghost) {
6521                  const ghostContent = ghost.querySelector('.gallery-item-content');
6522                  if (ghostContent) {
6523                    // Calculate depth from parent folder
6524                    const parentFolder = folderNestedSortable.closest('li.has-children');
6525                    if (parentFolder) {
6526                      const targetDepth = computeDepth(parentFolder) + 1;
6527                      ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
6528                    }
6529                  }
6530                }
6531              });
6532              
6533              // Return true immediately to allow the move
6534              return true;
6535            }
6536          }
6537          
6538          let actualRelatedParent = related.parentElement;
6539          
6540          // If related IS the folder item, we need to get its nested sortable container
6541          if (isRelatedFolderItem) {
6542            const nestedSortable = related.querySelector('ol.nested-sortable');
6543            if (nestedSortable) {
6544              actualRelatedParent = nestedSortable;
6545            }
6546          }
6547          
6548          const relatedParent = actualRelatedParent;
6549          
6550          const isMovingInsideFolder = relatedParent && 
6551                                       relatedParent.classList.contains('nested-sortable') && 
6552                                       relatedParent !== rootContainer;
6553          const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer;
6554          const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null);
6555          
6556          // Determine if we're moving within the same folder - this is the key check
6557          // If related IS the folder item, we're still moving within the same folder
6558          let isMovingWithinSameFolder = (isMovingInsideFolder && 
6559                                          draggedParent && 
6560                                          draggedParent.classList.contains('nested-sortable') &&
6561                                          draggedParent !== rootContainer &&
6562                                          relatedParent === draggedParent) ||
6563                                          // OR: if related is the folder item itself and we're dragging from that folder
6564                                          (isRelatedFolderItem && 
6565                                           draggedParent && 
6566                                           draggedParent.classList.contains('nested-sortable') &&
6567                                           draggedParent !== rootContainer &&
6568                                           related.querySelector('ol.nested-sortable') === draggedParent);
6569          
6570          // Special case: If related is the folder item, we're definitely trying to move within that folder
6571          // This happens when targeting first position - SortableJS uses the folder item as related
6572          if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6573            const folderNestedSortable = related.querySelector('ol.nested-sortable');
6574            if (folderNestedSortable === draggedParent) {
6575              // We're dragging within the folder, and related is the folder item
6576              // This means we're targeting first position - FORCE it to be treated as same-folder move
6577              // FORCE same-folder detection - override any previous calculation
6578              isMovingWithinSameFolder = true;
6579            }
6580          }
6581          
6582          // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens
6583          // (Don't log every single move - too noisy)
6584          if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6585            const folderItems = Array.from(draggedParent.children);
6586            const draggedIndex = folderItems.indexOf(dragged);
6587            const relatedIndex = folderItems.indexOf(related);
6588            
6589            // Get placeholder element to see where it actually is
6590            const placeholder = document.querySelector('.sortable-ghost');
6591            const placeholderParent = placeholder ? placeholder.parentElement : null;
6592            const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 
6593              ? Array.from(placeholderParent.children).indexOf(placeholder) 
6594              : -1;
6595            
6596            const folderItem = draggedParent.closest('li.has-children');
6597            const folderItemId = folderItem ? folderItem.dataset.id : null;
6598            const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null;
6599            const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null;
6600            
6601            // Check if we're trying to target first position
6602            // related could be the first item OR the folder item itself (when SortableJS uses folder as related)
6603            const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder;
6604            const isRelatedFolderItemCheck = related === folderItem;
6605            const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean
6606            
6607            // When targeting first via folder item, placeholder should be at index 0
6608            // But we're seeing it at index 1 - this is the issue!
6609            const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0;
6610            
6611            // Check if placeholder is outside the folder when it should be inside
6612            const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable'));
6613            const shouldBeInside = draggedIndex >= 0; // We're dragging from inside
6614            
6615            // Only log when there's a potential issue or interesting state change
6616            const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0;
6617            const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0;
6618            const jumpingOutToRoot = placeholderIsOutside && shouldBeInside;
6619            
6620            // First position issue: trying to target first but:
6621            // - placeholder is outside OR 
6622            // - placeholder is at index 1 instead of 0 (when targeting first via folder item)
6623            const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition);
6624            
6625            // ALWAYS log when targeting first position via folder item (this is the key case we're debugging)
6626            // OR when placeholder jumps out when it should stay in
6627            // OR when placeholder should be at first position but isn't
6628            const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition;
6629            
6630            if (shouldLog) {
6631              console.error('🔍 [Placeholder Position]', {
6632                draggedId,
6633                draggedIndex,
6634                relatedId,
6635                relatedIndex,
6636                placeholderIndex,
6637                relatedElementTag: related.tagName,
6638                isRelatedFolderItem: isRelatedFolderItemCheck,
6639                isTargetingFirstViaFolderItem,
6640                isMovingWithinSameFolder,
6641                placeholderMismatch,
6642                jumpingOut,
6643                jumpingOutToRoot,
6644                isFirstPosition,
6645                placeholderIsOutside,
6646                placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'),
6647                folderItemId,
6648                firstItemInFolderId,
6649                firstPositionIssue,
6650                shouldBeAtFirstPosition,
6651                expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex
6652              });
6653            }
6654          }
6655          
6656          // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic
6657          // This allows smooth reordering just like items not in folders
6658          if (isMovingWithinSameFolder) {
6659            // Simple debounce only - same as root level items
6660            const timeSinceLastMove = currentTime - lastMoveTime;
6661            // Only prevent very rapid oscillation to same element
6662            if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) {
6663              return false;
6664            }
6665            // Allow all moves within same folder - update tracking and continue
6666            
6667            // SPECIAL FIX: When targeting first position via folder item, fix placeholder position
6668            // SortableJS places it at index 1 when related is the folder item, but we want it at index 0
6669            // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive
6670            if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6671              const folderItems = Array.from(draggedParent.children);
6672              const firstItem = folderItems.length > 0 ? folderItems[0] : null;
6673              
6674              // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up)
6675              // This prevents the fix from being too aggressive when moving to other positions
6676              const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions
6677              
6678              if (firstItem && firstItem !== dragged && isNearFirstPosition) {
6679                // Fix immediately (synchronously) first - don't wait for next frame
6680                const placeholder = document.querySelector('.sortable-ghost');
6681                if (placeholder && placeholder.parentElement === draggedParent) {
6682                  const currentIndex = Array.from(draggedParent.children).indexOf(placeholder);
6683                  // Only fix if placeholder is at index 1 when we're targeting first position
6684                  // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3)
6685                  if (currentIndex === 1 && draggedIndex <= 1) {
6686                    draggedParent.insertBefore(placeholder, firstItem);
6687                  }
6688                }
6689              } else {
6690                // Not near first position - stop any active fix loop
6691                firstPositionFixLoopActive = false;
6692              }
6693            } else {
6694              // Not targeting first position anymore - reset flag
6695              firstPositionFixLoopActive = false;
6696            }
6697            
6698            lastMoveTime = currentTime;
6699            lastMoveRelatedId = relatedId;
6700            // Skip all the boundary case logic below - just continue to allow the move
6701            // Fall through to update intendedPlacementContainer and return true
6702          } else {
6703            // Only apply complex boundary logic for moves BETWEEN folders or to/from root
6704            
6705            // Hysteresis: Prevent rapid switching between inside/outside folder
6706            const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : [];
6707            const isFirstPositionInFolder = isMovingInsideFolder && 
6708                                           targetFolderItems.length > 0 && 
6709                                           related === targetFolderItems[0];
6710            // Special case: Empty folder - allow easy dropping into it
6711            const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0;
6712            
6713            if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) {
6714              const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime;
6715              
6716              // Make first position easier to target from outside
6717              if (isFirstPositionInFolder) {
6718                // Moving to first position from outside - use shorter debounce
6719                const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3;
6720                if (timeSinceContainerSwitch < effectiveDebounce) {
6721                  // Only block if it's very rapid (oscillation)
6722                  if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) {
6723                    return false;
6724                  }
6725                }
6726              } else if (isEmptyFolder) {
6727                // Empty folder - allow easy dropping, minimal debounce
6728                // Only block very rapid oscillation
6729                if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) {
6730                  return false;
6731                }
6732              } else {
6733                // Normal container switch - apply full hysteresis
6734                if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) {
6735                  return false;
6736                }
6737              }
6738            }
6739          }
6740          
6741          // Special handling for boundary cases (last item in folder, or dragging into folder)
6742          // BUT: Skip all this logic if we're moving within the same folder (already handled above)
6743          if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6744            const folderItem = draggedParent.closest('li.has-children');
6745            const currentFolderItems = Array.from(draggedParent.children);
6746            const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 
6747                                                   currentFolderItems[0] !== dragged &&
6748                                                   (related === currentFolderItems[0] || 
6749                                                    (isMovingInsideFolder && relatedParent === draggedParent && 
6750                                                     related === currentFolderItems[0]));
6751            
6752            // Case 0: This case is now handled earlier for same-folder moves
6753            // Only handle cross-folder moves here
6754            
6755            // Case 1: Dragging last item out of folder
6756            if (isLastItemInFolder && isMovingOutsideFolder) {
6757              const timeSinceLastMove = currentTime - lastMoveTime;
6758              
6759              // BUT: If we're trying to move to first position, don't block it
6760              // Check if the related element is the folder itself or right after it
6761              if (folderItem) {
6762                const folderNextSibling = folderItem.nextElementSibling;
6763                // If we're moving to position right after folder, it might be trying to go to first position
6764                // Allow it if it's been a reasonable time
6765                if (folderNextSibling && related === folderNextSibling) {
6766                  // This is ambiguous - could be first position or outside
6767                  // Require more time but not as much as pure outside move
6768                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
6769                    if (relatedId === lastMoveRelatedId) {
6770                      return false;
6771                    }
6772                  }
6773                } else {
6774                  // Definitely moving outside - require longer debounce
6775                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
6776                    if (relatedId === lastMoveRelatedId) {
6777                      return false; // Reject if oscillating
6778                    }
6779                  }
6780                }
6781              } else {
6782                // No folder item found - apply standard debounce
6783                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
6784                  if (relatedId === lastMoveRelatedId) {
6785                    return false;
6786                  }
6787                }
6788              }
6789            }
6790            
6791            // Case 2: Dragging item into folder from outside
6792            if (isMovingInsideFolder && relatedParent !== draggedParent) {
6793              const targetFolderItems = Array.from(relatedParent.children);
6794              const isEmptyFolder = targetFolderItems.length === 0;
6795              const isNearBottom = targetFolderItems.length > 0 && 
6796                                   (related === targetFolderItems[targetFolderItems.length - 1] ||
6797                                    targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
6798              const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0];
6799              
6800              // Special handling for empty folder - allow easy dropping
6801              if (isEmptyFolder) {
6802                const timeSinceLastMove = currentTime - lastMoveTime;
6803                // Very minimal debounce for empty folder - only prevent rapid oscillation
6804                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) {
6805                  return false;
6806                }
6807                // Allow move into empty folder - don't block it
6808              } else if (isFirstPos) {
6809                // Moving to first position - make it easy
6810                const timeSinceLastMove = currentTime - lastMoveTime;
6811                // Very short debounce for first position
6812                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) {
6813                  return false;
6814                }
6815                // Allow move to first position
6816              } else if (isNearBottom) {
6817                // For bottom position, still use some debounce to prevent oscillation
6818                const timeSinceLastMove = currentTime - lastMoveTime;
6819                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) {
6820                  if (relatedId === lastMoveRelatedId) {
6821                    return false;
6822                  }
6823                }
6824              }
6825            }
6826            
6827          // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within
6828          // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last
6829          if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) {
6830            // We're dragging from within a folder but detected as moving outside
6831            // Check if the related element is adjacent to the folder (which might indicate targeting first/last)
6832            const folderIndex = Array.from(rootContainer.children).indexOf(folderItem);
6833            const relatedIndex = Array.from(rootContainer.children).indexOf(related);
6834            
6835            // If related is immediately before or after folder, might be targeting first/last
6836            if (folderIndex >= 0 && relatedIndex >= 0) {
6837              const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1);
6838              
6839              if (isAdjacentToFolder) {
6840                // Might be targeting first/last position - be lenient
6841                const timeSinceLastMove = currentTime - lastMoveTime;
6842                // Only block if it's very rapid oscillation
6843                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) {
6844                  return false;
6845                }
6846                // Otherwise allow - user might be trying to target first/last position
6847                // Don't block based on this check
6848              }
6849            }
6850          }
6851          }
6852          
6853          // Case 3: Dragging from root into folder
6854          if (draggedParent === rootContainer && isMovingInsideFolder) {
6855            const targetFolderItems = Array.from(relatedParent.children);
6856            const isEmptyFolder = targetFolderItems.length === 0;
6857            const isNearBottom = targetFolderItems.length > 0 && 
6858                                 (related === targetFolderItems[targetFolderItems.length - 1] ||
6859                                  targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
6860            const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0];
6861            
6862            // Special handling for empty folder - allow easy dropping
6863            if (isEmptyFolder) {
6864              const timeSinceLastMove = currentTime - lastMoveTime;
6865              // Very minimal debounce for empty folder - only prevent rapid oscillation
6866              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) {
6867                if (relatedId === lastMoveRelatedId) {
6868                  return false;
6869                }
6870              }
6871              // Allow move into empty folder - don't block it
6872            } else if (isFirstPosition) {
6873              // Special handling for first position - make it easier to target
6874              const timeSinceLastMove = currentTime - lastMoveTime;
6875              // Make first position easier - only prevent very rapid oscillation
6876              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) {
6877                if (relatedId === lastMoveRelatedId) {
6878                  return false;
6879                }
6880              }
6881              // Allow move to first position - don't block it
6882              // Continue to track but don't apply strict debouncing
6883            } else if (isNearBottom) {
6884              // Keep strict debouncing for bottom position
6885              const timeSinceLastMove = currentTime - lastMoveTime;
6886              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
6887                if (relatedId === lastMoveRelatedId) {
6888                  return false;
6889                }
6890              }
6891            }
6892          }
6893          
6894          // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position)
6895          // If we're at first position and trying to move outside, require deliberate movement
6896          if (isMovingInsideFolder) {
6897            const currentFolderItems = Array.from(relatedParent.children);
6898            const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0];
6899            
6900            // If we're moving FROM first position to outside (reverse hysteresis)
6901            if (isFirstPos && lastContainerDecision === 'outside') {
6902              const timeSinceLastMove = currentTime - lastMoveTime;
6903              // If we just moved outside, require more time before allowing back to first position
6904              // BUT: if it's been a reasonable time, allow it (don't make it too hard)
6905              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) {
6906                // Very rapid - might be oscillation
6907                return false;
6908              }
6909            }
6910          }
6911          
6912          // Case 5: Prevent oscillation between first and second position within folder
6913          // BUT: Only apply if NOT moving within same folder (to allow smooth reordering)
6914          if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) {
6915            const folderItems = Array.from(relatedParent.children);
6916            const isFirstPos = folderItems.length > 0 && related === folderItems[0];
6917            const isSecondPos = folderItems.length > 1 && related === folderItems[1];
6918            
6919            // If moving from first to second (or vice versa) very rapidly, prevent oscillation
6920            if ((isFirstPos || isSecondPos) && lastMoveRelatedId) {
6921              const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]');
6922              if (lastRelated && lastRelated.parentElement === relatedParent) {
6923                const lastRelatedIndex = folderItems.indexOf(lastRelated);
6924                const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0);
6925                
6926                if (isOscillating) {
6927                  const timeSinceLastMove = currentTime - lastMoveTime;
6928                  // Allow moving to first position easily, but prevent rapid oscillation between first/second
6929                  if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) {
6930                    return false; // Prevent jumping from first to second too easily
6931                  }
6932                  // But allow first position more easily
6933                  if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) {
6934                    return false; // Only prevent very rapid oscillation
6935                  }
6936                }
6937              }
6938            }
6939          }
6940          
6941          // Prevent rapid oscillation when dragging within same container
6942          // NOTE: For same-folder moves, we already updated tracking above, so skip this 
6942section
6943          if (!isMovingWithinSameFolder) {
6944            const timeSinceLastMove = currentTime - lastMoveTime;
6945            
6946            // If moving to the same related element very quickly, reject to prevent oscillation
6947            if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) {
6948              return false;
6949            }
6950            
6951            // Update tracking variables (only for non-same-folder moves)
6952            lastMoveTime = currentTime;
6953            lastMoveRelatedId = relatedId;
6954          }
6955          // For same-folder moves, tracking was already updated in the earlier block
6956          
6957          // Update container decision tracking
6958          if (currentDecision) {
6959            if (currentDecision !== lastContainerDecision) {
6960              lastContainerDecisionTime = currentTime;
6961            }
6962            lastContainerDecision = currentDecision;
6963          }
6964          
6965          // Track where the placeholder indicates the item should be placed
6966          // The related element is next to where the placeholder will be positioned
6967          // Check if it's at root level or nested level
6968          if (related) {
6969            intendedRelatedElement = related;
6970            const relatedParent = related.parentElement;
6971            const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
6972            
6973            // Determine if the placeholder is at root level or nested level
6974            if (relatedParent === rootContainer) {
6975              // Placeholder is at root level (not nested)
6976              intendedPlacementContainer = rootContainer;
6977            } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) {
6978              // Placeholder is in a nested list (inside a folder)
6979              intendedPlacementContainer = relatedParent;
6980            }
6981            
6982            // Update ghost element indentation to match the target position
6983            requestAnimationFrame(() => {
6984              const ghost = document.querySelector('.sortable-ghost');
6985              if (ghost && intendedPlacementContainer) {
6986                const ghostContent = ghost.querySelector('.gallery-item-content');
6987                if (ghostContent) {
6988                  // Calculate the depth where the ghost will be placed
6989                  // Use the related element to determine the depth (if it exists and is in the target container)
6990                  let targetDepth = 0;
6991                  if (intendedPlacementContainer === rootContainer) {
6992                    targetDepth = 0; // Root level
6993                  } else if (related && related.parentElement === intendedPlacementContainer) {
6994                    // Use the related element's depth to determine target depth
6995                    targetDepth = computeDepth(related);
6996                  } else {
6997                    // Fallback: find first child in the container or calculate from parent folder
6998                    const firstChild = intendedPlacementContainer.querySelector('li');
6999                    if (firstChild) {
7000                      targetDepth = computeDepth(firstChild);
7001                    } else {
7002                      // Empty container (like empty folder) - calculate from parent folder
7003                      const parentFolder = intendedPlacementContainer.closest('li.has-children');
7004                      if (parentFolder) {
7005                        targetDepth = computeDepth(parentFolder) + 1;
7006                      }
7007                    }
7008                  }
7009                  
7010                  // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet,
7011                  // ensure placeholder is moved there and indentation is correct
7012                  if (intendedPlacementContainer.children.length === 0) {
7013                    const placeholder = document.querySelector('.sortable-ghost');
7014                    if (placeholder && placeholder.parentElement !== intendedPlacementContainer) {
7015                      // Move placeholder into empty container
7016                      intendedPlacementContainer.appendChild(placeholder);
7017                    }
7018                  }
7019                  ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
7020                }
7021              }
7022            });
7023          }
7024          
7025          // Let SortableJS handle the placement based on placeholder position
7026          // We'll verify and correct if needed in onEnd
7027          return true; // Allow the move
7028        },
7029        
7030        // Capture indentation before drag starts (fires when item is chosen)
7031        onChoose: function(evt) {
7032          if (!evt.item) return;
7033          
7034          const draggedItem = evt.item;
7035          const content = draggedItem.querySelector('.gallery-item-content');
7036          
7037          // Store the original padding-left for the ghost
7038          if (content) {
7039            const computedStyle = window.getComputedStyle(content);
7040            const originalPaddingLeft = computedStyle.paddingLeft;
7041            draggedItem.dataset.originalPaddingLeft = originalPaddingLeft;
7042          }
7043        },
7044        
7045        // Visual feedback on drag start
7046        onStart: function(evt) {
7047          if (!evt.item) return;
7048          
7049          // Don't set dimensions on the dragged item - let it maintain natural size
7050          // Setting width/height can cause layout shifts and menu expansion
7051          const draggedItem = evt.item;
7052          
7053          // Store original container and check if it's the last item in folder
7054          draggedItemOriginalContainer = draggedItem.parentElement;
7055          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7056          if (draggedItemOriginalContainer && 
7057              draggedItemOriginalContainer.classList.contains('nested-sortable') && 
7058              draggedItemOriginalContainer !== rootContainer) {
7059            const siblings = Array.from(draggedItemOriginalContainer.children);
7060            isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem;
7061          } else {
7062            isLastItemInFolder = false;
7063          }
7064          
7065          // Apply the stored padding-left to the ghost element
7066          // The ghost is created by SortableJS at this point
7067          requestAnimationFrame(() => {
7068            const ghost = document.querySelector('.sortable-ghost');
7069            if (ghost) {
7070              const ghostContent = ghost.querySelector('.gallery-item-content');
7071              const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft;
7072              if (ghostContent && originalPaddingLeft) {
7073                ghostContent.style.paddingLeft = originalPaddingLeft;
7074              }
7075            }
7076          });
7077          
7078          // Reset oscillation tracking variables
7079          lastMoveTime = 0;
7080          lastMoveRelatedId = null;
7081          lastContainerDecision = null;
7082          lastContainerDecisionTime = 0;
7083          
7084          // Create a backup
7085          try {
7086            window._dragStartElementIds = [];
7087            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7088              if (li.dataset.id) {
7089                window._dragStartElementIds.push(li.dataset.id);
7090              }
7091            });
7092            
7093            galleriesBackup = JSON.parse(JSON.stringify(galleries));
7094          } catch (error) {
7095            console.error('Error creating backup:', error);
7096          }
7097          
7098          // Add indicator classes
7099          document.body.classList.add('menu-item-dragging');
7100          
7101          // Add hover indicator class to submenu items
7102          document.querySelectorAll('.has-children').forEach(item => {
7103            item.classList.add('potential-parent');
7104          });
7105          
7106          // Prevent layout recalculation during drag
7107          document.body.style.setProperty('--is-dragging', '1');
7108        },
7109        
7110        // Cleanup on end
7111        onEnd: function(evt) {
7112          // Verify and correct placement based on placeholder's intended position
7113          // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level
7114          const dragged = evt.item;
7115          const previousParentLi = evt.from ? evt.from.closest('li') : null;
7116          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7117          
7118          if (!dragged || !rootContainer) {
7119            intendedPlacementContainer = null;
7120            intendedRelatedElement = null;
7121            // Restore normal sizing
7122            dragged.style.width = '';
7123            dragged.style.height = '';
7124            dragged.style.minHeight = '';
7125            document.body.style.removeProperty('--is-dragging');
7126            return;
7127          }
7128          
7129          // Get the actual parent container where SortableJS placed the item
7130          const actualParentList = dragged.parentElement;
7131          
7132          // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing
7133          requestAnimationFrame(() => {
7134            dragged.style.width = '';
7135            dragged.style.height = '';
7136            dragged.style.minHeight = '';
7137            document.body.style.removeProperty('--is-dragging');
7138          });
7139          
7140          if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) {
7141            intendedPlacementContainer = null;
7142            intendedRelatedElement = null;
7143          }
7144
7145          // Check if the intended placement (where placeholder showed) differs from actual placement
7146          // This happens when SortableJS auto-nests based on mouse position over a folder
7147          if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) {
7148            // The item ended up in a different container than the placeholder indicated
7149            // Move it to where the placeholder showed it should be (next to intendedRelatedElement)
7150            if (intendedPlacementContainer === rootContainer) {
7151              // Placeholder was at root level - move item to root, positioned next to related element
7152              if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) {
7153                // Insert before the related element (where placeholder was)
7154                rootContainer.insertBefore(dragged, intendedRelatedElement);
7155                console.log("Corrected placement: moved from folder to root level where placeholder indicated");
7156              } else if (intendedRelatedElement) {
7157                // Related element exists but not at root - find its folder and insert before it at root
7158                const folderItem = intendedRelatedElement.closest('li.has-children');
7159                if (folderItem && folderItem.parentElement === rootContainer) {
7160                  rootContainer.insertBefore(dragged, folderItem);
7161                  console.log("Corrected placement: moved to root level above folder where placeholder indicated");
7162                } else {
7163                  rootContainer.appendChild(dragged);
7164                }
7165              } else {
7166                rootContainer.appendChild(dragged);
7167              }
7168              // Update indentation since it's now at root
7169              applyIndentationFromDepth(dragged);
7170            } else {
7171              // Placeholder was in a nested list - move it there
7172              if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) {
7173                // Insert before the related element in the nested list
7174                intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement);
7175              } else {
7176                intendedPlacementContainer.appendChild(dragged);
7177              }
7178              console.log("Corrected placement: moved to nested list where placeholder indicated");
7179              // Update indentation for nested item
7180              applyIndentationFromDepth(dragged);
7181              
7182              // Ensure parent folder has proper structure
7183              const folderItem = intendedPlacementContainer.closest('li.has-children');
7184              if (folderItem) {
7185                const nestedList = folderItem.querySelector('ol.nested-sortable');
7186                if (nestedList && intendedPlacementContainer !== nestedList) {
7187                  nestedList.appendChild(dragged);
7188                }
7189                
7190                const parentDepth = computeDepth(folderItem);
7191                const nestedDepth = parentDepth + 1;
7192                
7193                if (nestedList) {
7194                  nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7195                }
7196                
7197                folderItem.classList.add('has-children');
7198              }
7199            }
7200          } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) {
7201            // Item is correctly in a nested list - ensure proper structure
7202            const folderItem = actualParentList.closest('li.has-children');
7203            if (folderItem) {
7204              const nestedList = folderItem.querySelector('ol.nested-sortable');
7205              if (nestedList && actualParentList !== nestedList) {
7206                nestedList.appendChild(dragged);
7207              }
7208              
7209              const parentDepth = computeDepth(folderItem);
7210              const nestedDepth = parentDepth + 1;
7211              
7212              if (nestedList) {
7213                nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7214              }
7215              
7216              folderItem.classList.add('has-children');
7217            }
7218            applyIndentationFromDepth(dragged);
7219          } else {
7220            // Item is correctly at root level
7221            applyIndentationFromDepth(dragged);
7222          }
7223
7224          // Reset tracking variables
7225          intendedPlacementContainer = null;
7226          intendedRelatedElement = null;
7227          isLastItemInFolder = false;
7228          draggedItemOriginalContainer = null;
7229          lastContainerDecision = null;
7230          lastContainerDecisionTime = 0;
7231
7232          // Apply indentation updates for ALL affected items after drag
7233          // Use requestAnimationFrame to batch DOM updates and prevent visual jumps
7234          requestAnimationFrame(() => {
7235            if (dragged) {
7236              // Update the dragged item and all its descendants
7237              applyIndentationFromDepth(dragged);
7238              
7239              // Update all items in the new container (siblings of dragged item)
7240              const newContainer = dragged.parentElement;
7241              if (newContainer && newContainer.classList.contains('nested-sortable')) {
7242                const siblings = Array.from(newContainer.children);
7243                siblings.forEach(sibling => {
7244                  if (sibling !== dragged && sibling.tagName === 'LI') {
7245                    applyIndentationFromDepth(sibling);
7246                  }
7247                });
7248              }
7249              
7250              // Update all items in the old container (if it still exists and has items)
7251              if (evt.from && evt.from.classList.contains('nested-sortable')) {
7252                const oldSiblings = Array.from(evt.from.children);
7253                oldSiblings.forEach(sibling => {
7254                  if (sibling.tagName === 'LI') {
7255                    applyIndentationFromDepth(sibling);
7256                  }
7257                });
7258              }
7259              
7260              // Also update all items in the entire tree to ensure consistency
7261              // This handles edge cases where indentation might be off
7262              const allItems = document.querySelectorAll('#galleryTree .nested-sortable li');
7263              allItems.forEach(li => {
7264                const depth = computeDepth(li);
7265                li.setAttribute('data-nesting-level', depth);
7266                const content = li.querySelector('.gallery-item-content');
7267                if (content) {
7268                  content.style.paddingLeft = (depth * 16) + 'px';
7269                }
7270              });
7271            }
7272          });
7273
7274          // Clean up previous parent if it lost its last child
7275          if (previousParentLi) {
7276            cleanupParentIfEmpty(previousParentLi);
7277          }
7278          
7279          // Remove all indicator classes
7280          document.body.classList.remove('menu-item-dragging');
7281          document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => {
7282            el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested');
7283          });
7284          
7285          // Standard cleanup for ghost elements
7286          document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => {
7287            item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag');
7288          });
7289          
7290          // Check if drop was outside a valid container
7291          if (!evt.to || !evt.to.classList.contains('nested-sortable')) {
7292            console.warn('Item was dropped outside a valid container - reverting');
7293            restoreFromBackup();
7294            return;
7295          }
7296          
7297          // Verify no elements were lost
7298          try {
7299            // Check DOM elements
7300            const endElementIds = [];
7301            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7302              if (li.dataset.id) {
7303                endElementIds.push(li.dataset.id);
7304              }
7305            });
7306            
7307            if (window._dragStartElementIds && window._dragStartElementIds.length > 0) {
7308              const startCount = window._dragStartElementIds.length;
7309              const endCount = endElementIds.length;
7310              
7311              if (endCount < startCount) {
7312                console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`);
7313                restoreFromBackup();
7314                return;
7315              }
7316            }
7317            
7318            // Update the data structure
7319            updateGalleryStructure();
7320            
7321            // Verify data model
7322            const originalCount = countGalleries(galleriesBackup);
7323            const newCount = countGalleries(galleries);
7324            
7325            if (newCount < originalCount) {
7326              console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`);
7327              restoreFromBackup();
7328              return;
7329            }
7330            
7331            // updateGalleryStructure already persists; do not save or re-render again
7332            // here. A second save/render can rebind Sortable and snap the drop back.
7333          } catch (error) {
7334            console.error('Error updating gallery structure:', error);
7335            restoreFromBackup();
7336          }
7337        }
7338      });
7339      
7340      sortableInstances.push(sortable);
7341    } catch (error) {
7342      console.error(`Error initializing sortable for container ${i}:`, error);
7343    }
7344  }
7345
7346  // Add event delegation for folder toggles (will be added by setupFolderToggleHandler)
7347  setupFolderToggleHandler();
7348
7349  return sortableInstances;
7350}
7351
7352// Helper function to set up folder toggle event delegation
7353// This must be called after galleries are rendered, in both view and edit modes
7354function setupFolderToggleHandler() {
7355  const galleryTree = document.getElementById('galleryTree');
7356  if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) {
7357    galleryTree.addEventListener('click', function(e) {
7358      
7359      const target = e.target;
7360      // Resolve the clicked row first — do not use target.closest('.menu-item.folder'),
7361      // which matches ancestor folder labels when clicking nested pages/galleries.
7362      const listItem = target.closest('li[data-id]');
7363      if (!listItem) {
7364        return;
7365      }
7366
7367      const contentDiv = listItem.querySelector(':scope > .gallery-item-content');
7368      if (!contentDiv) {
7369        return;
7370      }
7371
7372      const toggleButton = contentDiv.querySelector('.submenu-toggle');
7373      const folderText = contentDiv.querySelector('.menu-item.folder');
7374      const isToggleClick = toggleButton && toggleButton.contains(target);
7375      const isFolderTextClick = folderText && folderText.contains(target);
7376      
7377      
7378      if (isToggleClick || isFolderTextClick) {
7379        const folderId = parseInt(listItem.dataset.id, 10);
7380        
7381        if (folderId && !isNaN(folderId)) {
7382          // Prevent SortableJS from intercepting this click
7383          e.stopPropagation();
7384          e.stopImmediatePropagation();
7385          toggleFolder(folderId, e);
7386        }
7387      }
7388    }, true); // Use capture phase to run before SortableJS
7389    
7390    galleryTree.dataset.folderToggleHandlerAdded = 'true';
7391  }
7392}
7393
7394// NEW HELPER FUNCTION: Gets the nesting level of an element
7395function getElementNestingLevel(element) {
7396  let level = 0;
7397  let current = element;
7398  
7399  // Count how many nested-sortable parents this element has
7400  while (current && current.parentElement) {
7401    if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) {
7402      level++;
7403    }
7404    current = current.parentElement;
7405  }
7406  
7407  return level;
7408}
7409
7410// NEW HELPER FUNCTION: Determines if an item would become nested
7411function wouldBeNestedItem(draggedEl, relatedEl, event) {
7412  if (!relatedEl || !draggedEl) return false;
7413  
7414  // Get mouse position
7415  const mouseX = event.clientX;
7416  
7417  // Get the bounding rect of the related element
7418  const rect = relatedEl.getBoundingClientRect();
7419  
7420  // Determine the nesting threshold - how far from the left edge
7421  // the mouse needs to be to consider it a nesting operation
7422  const nestingThreshold = 25; // pixels from left edge
7423  
7424  // Check if mouse is within the nesting threshold from the left edge
7425  const distanceFromLeft = mouseX - rect.left;
7426  
7427  // Check if we should nest (mouse is within threshold of left edge)
7428  const wouldNest = distanceFromLeft <= nestingThreshold;
7429  
7430  // Log the calculation for debugging
7431  console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`);
7432  
7433  return wouldNest;
7434}
7435
7436/**
7437 * Helper function to find gallery by ID in a tree structure
7438 * @param {Array} galleries - The gallery array to search
7439 * @param {number} id - The ID to find
7440 * @returns {Object|null} - The found gallery or null
7441 */
7442function findGalleryById(galleries, id) {
7443  if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) {
7444    return window.HelpSystem.getDemoRecord(id);
7445  }
7446  if (!Array.isArray(galleries)) return null;
7447  
7448  // First check at the current level
7449  const directMatch = galleries.find(g => g.id == id);
7450  if (directMatch) return directMatch;
7451  
7452  // Then check in children
7453  for (const gallery of galleries) {
7454    if (gallery.children && gallery.children.length > 0) {
7455      const childMatch = findGalleryById(gallery.children, id);
7456      if (childMatch) return childMatch;
7457    }
7458  }
7459  
7460  return null;
7461}
7462
7463/** Detect page menu items even when isPage flag was not persisted */
7464function isPageMenuItem(item) {
7465  if (!item) return false;
7466  if (item.isPage === true) return true;
7467  if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true;
7468  if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true;
7469  return false;
7470}
7471
7472/**
7473 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior.
7474 * Never infer this from children or parentId; legacy items must retain their existing behavior.
7475 */
7476function isNavigableMenuParent(item) {
7477  if (!item || item.actsAsMenuParent !== true) return false;
7478  if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) {
7479    return false;
7480  }
7481  return isPageMenuItem(item) || item.isIntegrated === true;
7482}
7483
7484/**
7485 * Get all gallery IDs from a tree structure
7486 * @param {Array} galleries - Array of galleries
7487 * @returns {Array} - Flat array of all IDs
7488 */
7489function getAllGalleryIds(galleries) {
7490  if (!Array.isArray(galleries)) return [];
7491  
7492  let ids = [];
7493  
7494  galleries.forEach(gallery => {
7495    if (gallery && gallery.id) {
7496      ids.push(gallery.id);
7497    }
7498    
7499    // Add child IDs if any
7500    if (gallery.children && Array.isArray(gallery.children)) {
7501      ids = ids.concat(getAllGalleryIds(gallery.children));
7502    }
7503  });
7504  
7505  return ids;
7506}
7507
7508/**
7509 * Enhanced count function that handles edge cases
7510 * @param {Array} galleryArray - The array to count
7511 * @returns {number} - The total count of galleries
7512 */
7513function countGalleries(galleryArray) {
7514  if (!Array.isArray(galleryArray)) return 0;
7515
7516  // Menu items are stored flat and also get a derived children[] at render time.
7517  // Counting children recursively double-counts those items and falsely triggers
7518  // restoreFromBackup after a successful drop.
7519  const ids = new Set();
7520  function walk(items) {
7521    if (!Array.isArray(items)) return;
7522    items.forEach(function(gallery) {
7523      if (!gallery) return;
7524      if (gallery.id != null) ids.add(gallery.id);
7525      if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children);
7526    });
7527  }
7528  walk(galleryArray);
7529  return ids.size;
7530}
7531
7532/**
7533 * Enhanced backup restoration function with better debugging and notification
7534 */
7535function restoreFromBackup() {
7536  console.warn('Restoring galleries from backup due to invalid operation');
7537  
7538  // Verify we have a valid backup
7539  if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) {
7540    console.error('No valid backup available for restoration');
7541    
7542    // Create a backup from the current DOM as a last resort
7543    try {
7544      console.log('Attempting to rebuild from DOM structure...');
7545      rebuildGalleriesFromDOM();
7546      return;
7547    } catch (rebuildError) {
7548      console.error('Failed to rebuild from DOM:', rebuildError);
7549      // Continue with normal user notification
7550    }
7551  } else {
7552    // Restore from backup
7553    galleries = JSON.parse(JSON.stringify(galleriesBackup));
7554  }
7555  
7556  // Re-render the tree with the restored data
7557  renderGalleries();
7558  
7559  // Force reinitialize nested sortables after restoration
7560  setTimeout(() => {
7561    initializeNestedSortables();
7562  }, 300);
7563}
7564
7565/**
7566 * Emergency recovery function that tries to rebuild galleries from DOM
7567 * This is a last resort when backup restoration fails
7568 */
7569function rebuildGalleriesFromDOM() {
7570  console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure');
7571  
7572  // Create a new galleries array
7573  const rebuiltGalleries = [];
7574  
7575  // This will track items we've already processed
7576  const processedIds = new Set();
7577  
7578  // Function to process a container and its children
7579  function processContainer(container, parentId = null) {
7580    // Get all immediate list items
7581    const items = container.querySelectorAll(':scope > li');
7582    
7583    items.forEach(item => {
7584      // Get item ID from data attribute
7585      const id = parseInt(item.dataset.id);
7586      if (isNaN(id)) return; // Skip if no valid ID
7587      
7588      // Skip already processed items to avoid duplicates
7589      if (processedIds.has(id)) return;
7590      processedIds.add(id);
7591      
7592      // Try to find the original gallery data
7593      let galleryData = null;
7594      
7595      // First check current galleries array
7596      const existingGallery = findGalleryById(galleries, id);
7597      if (existingGallery) {
7598        galleryData = { ...existingGallery };
7599        delete galleryData.children; // We'll rebuild the hierarchy
7600      }
7601      // If not found, check if we have a backup
7602      else if (galleriesBackup && Array.isArray(galleriesBackup)) {
7603        const backupGallery = findGalleryById(galleriesBackup, id);
7604        if (backupGallery) {
7605          galleryData = { ...backupGallery };
7606          delete galleryData.children; // We'll rebuild the hierarchy
7607        }
7608      }
7609      
7610      // If we still don't have data, create minimal placeholder
7611      if (!galleryData) {
7612        // Try to extract title from DOM
7613        const titleElement = item.querySelector('.menu-item');
7614        const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`;
7615        
7616        galleryData = {
7617          id: id,
7618          title: title,
7619          visible: !item.classList.contains('hidden-gallery')
7620        };
7621      }
7622      
7623      // Update parent ID to match DOM structure
7624      galleryData.parentId = parentId;
7625      
7626      // Add to rebuilt galleries
7627      rebuiltGalleries.push(galleryData);
7628      
7629      // Process children if any
7630      const childContainer = item.querySelector('ol.nested-sortable');
7631      if (childContainer) {
7632        processContainer(childContainer, id);
7633      }
7634    });
7635  }
7636  
7637  // Start with root container
7638  const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7639  if (rootContainer) {
7640    processContainer(rootContainer);
7641    
7642    // Replace global galleries array with our rebuilt one
7643    galleries = rebuiltGalleries;
7644    
7645    // Re-render with the rebuilt data
7646    renderGalleries();
7647    
7648    console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`);
7649    return true;
7650  } else {
7651    console.error('RECOVERY FAILED: Root container not found');
7652    return false;
7653  }
7654}
7655
7656// Make sure these functions are available to the global scope
7657window.initializeNestedSortables = initializeNestedSortables;
7658window.restoreFromBackup = restoreFromBackup;
7659window.getElementNestingLevel = getElementNestingLevel;
7660window.wouldBeNestedItem = wouldBeNestedItem;
7661
7662
7663
7664function hasDescendant(itemId, possibleDescendantId) {
7665// Check if possibleDescendantId is a descendant of itemId
7666const children = galleries.filter(g => g.parentId === itemId);
7667
7668if (children.some(child => child.id === possibleDescendantId)) {
7669return true;
7670}
7671
7672return children.some(child => hasDescendant(child.id, possibleDescendantId));
7673}
7674
7675function updateGalleryStructure() {
7676// Create a new array to hold the updated gallery structure
7677const newGalleries = [];
7678
7679// Function to recursively process nested lists
7680function processNestedList(container, parentId = null) {
7681if (!container || !container.children) {
7682  console.warn('Invalid container in processNestedList', container);
7683  return;
7684}
7685
7686const items = container.children;
7687
7688Array.from(items).forEach(item => {
7689  if (item.tagName !== 'LI') return; // Skip non-list items
7690  
7691  const id = parseInt(item.dataset.id);
7692  if (isNaN(id)) {
7693    console.warn('Invalid ID in list item:', item);
7694    return;
7695  }
7696  
7697  const gallery = galleries.find(g => g.id === id);
7698  
7699  if (gallery) {
7700    // Create a new gallery object without children
7701    const newGallery = { ...gallery };
7702    if (newGallery.children) delete newGallery.children;
7703    
7704    // Update parent ID
7705    newGallery.parentId = parentId;
7706    
7707    // Add to new galleries array
7708    newGalleries.push(newGallery);
7709    
7710    // Process any nested OL with class 'nested-sortable' within this item
7711    const nestedList = item.querySelector('ol.nested-sortable');
7712    if (nestedList) {
7713      processNestedList(nestedList, id);
7714    }
7715  } else {
7716    console.warn(`Gallery with ID ${id} not found in galleries array`);
7717  }
7718});
7719}
7720
7721// Start processing from the root list
7722const rootList = document.querySelector('#galleryTree > ol.nested-sortable');
7723if (rootList) {
7724try {
7725  processNestedList(rootList);
7726  
7727  // Verify that we haven't lost any galleries
7728  if (newGalleries.length < galleries.length) {
7729    console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`);
7730    
7731    // Find missing galleries
7732    const originalIds = galleries.map(g => g.id);
7733    const newIds = newGalleries.map(g => g.id);
7734    const missingIds = originalIds.filter(id => !newIds.includes(id));
7735    
7736    if (missingIds.length > 0) {
7737      console.error('Missing gallery IDs:', missingIds);
7738      
7739      // Add missing galleries to the new structure
7740      missingIds.forEach(id => {
7741        const missingGallery = galleries.find(g => g.id === id);
7742        if (missingGallery) {
7743          // Add it as a top-level item
7744          const newGallery = { ...missingGallery, parentId: null };
7745          if (newGallery.children) delete newGallery.children;
7746          newGalleries.push(newGallery);
7747          console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`);
7748        }
7749      });
7750    }
7751  }
7752  
7753  // Update the galleries array with the new structure
7754  galleries = newGalleries;
7755  if (typeof window !== 'undefined') {
7756    window.galleries = galleries;
7757  }
7758  
7759  // Save the updated structure
7760  saveGalleries();
7761} catch (error) {
7762  console.error('Error in updateGalleryStructure:', error);
7763  throw error; // Re-throw to trigger the backup restore
7764}
7765} else {
7766console.warn('Root list not found');
7767throw new Error('Root list not found'); // Throw error to trigger backup restore
7768}
7769}
7770
7771function toggleSubmenu(id, event) {
7772// Ensure the event doesn't interfere with drag operations
7773event.stopPropagation();
7774
7775const listItem = getGalleryTreeListItem(id, event);
7776if (listItem) {
7777// Toggle the expanded class
7778listItem.classList.toggle('expanded');
7779
7780// Find the submenu toggle icon and rotate it
7781const toggleIcon = listItem.querySelector('.toggle-icon');
7782if (toggleIcon) {
7783  toggleIcon.classList.toggle('rotated');
7784}
7785}
7786}
7787
7788/**
7789 * Improved slugify function with robust character handling
7790 * @param {string} text - The text to convert to a slug
7791 * @returns {string} A URL-friendly slug
7792 */
7793function slugify(text) {
7794  // Guard against null or empty input
7795  if (!text || text.trim() === '') {
7796    return 'page-' + Date.now();
7797  }
7798  
7799  // First convert to lowercase
7800  let slug = text.toLowerCase();
7801  
7802  // Replace spaces with hyphens
7803  slug = slug.split(' ').join('-');
7804  
7805  // Filter out unwanted characters (keep only a-z, 0-9, and hyphens)
7806  let filtered = '';
7807  for (let i = 0; i < slug.length; i++) {
7808    const char = slug.charAt(i);
7809    if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') {
7810      filtered += char;
7811    }
7812  }
7813  slug = filtered;
7814  
7815  /*
7816  // Clean up multiple hyphens
7817  while (slug.indexOf('--') !== -1) {
7818    slug = slug.replace('--', '-');
7819  }
7820    */
7821  
7822  // Remove leading and trailing hyphens
7823  if (slug.startsWith('-')) {
7824    slug = slug.substring(1);
7825  }
7826  if (slug.endsWith('-')) {
7827    slug = slug.substring(0, slug.length - 1);
7828  }
7829  
7830  // If slug is empty after processing, use a default
7831  if (!slug || slug === '-') {
7832    return 'page-' + Date.now();
7833  }
7834  
7835  return slug;
7836}
7837
7838// Update the addPage function to use our new robust slugify function
7839/**
7840 * Adds a new page to the galleries array
7841 * @returns {number|null} The new page ID or null if error
7842 */
7843function addPage() {
7844  try {
7845    // Get the page title
7846    const titleInput = document.getElementById('galleryTitle');
7847    if (!titleInput) {
7848      console.error('Page title input not found');
7849      return;
7850    }
7851    const title = titleInput.value.trim();
7852
7853    // Validate title is not empty
7854    if (!title) {
7855      alert('Please enter a page title');
7856      return;
7857    }
7858
7859    // Get parent ID with error handling
7860    const parentSelect = document.getElementById('galleryParent');
7861    const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null;
7862
7863    // Generate a unique ID for the page
7864    const id = Date.now();
7865    const pageId = `page_${id}`;
7866
7867    // Calculate slug using our more robust slugify function
7868    const pageSlug = slugify(title);
7869 
7870    // Create the page object with explicit properties
7871    const page = {
7872      id: id,
7873      title: title,
7874      isPage: true,
7875      isIntegrated: true,
7876      isSubmenu: false,
7877      pageId: pageId,
7878      visible: true,
7879      parentId: parentId,
7880      siteId: siteId,
7881      // Set slug and URL explicitly
7882      slug: pageSlug,
7883      url: '/' + pageSlug
7884    };
7885
7886    console.log(`Creating new page with ID: ${id}, title: "${title}"`);
7887
7888    // Add to galleries array
7889    galleries.push(page);
7890    
7891    // CRITICAL: Ensure window.galleries is synchronized
7892    if (typeof window.galleries === 'undefined') {
7893      window.galleries = galleries;
7894    } else {
7895      // Make sure the page is in window.galleries too
7896      const existingIndex = window.galleries.findIndex(g => g.id === id);
7897      if (existingIndex >= 0) {
7898        window.galleries[existingIndex] = page;
7899      } else {
7900        window.galleries.push(page);
7901      }
7902    }
7903
7904    // Initialize empty page elements array in PageManager
7905    if (window.PageManager) {
7906      window.PageManager.elements[pageId] = [];
7907    }
7908
7909    // Update UI first
7910    renderGalleries();
7911    clearAddForm();
7912    toggleAddForm();
7913
7914    // Set as active page
7915    console.log(`Setting new page "${page.title}" as active`);
7916    activeGalleryId = id;
7917    window.activeGalleryId = id;
7918    
7919    // Update active states in the menu
7920    updateActiveStates();
7921
7922    // Save to server
7923    saveGalleries().then(() => {
7924      console.log(`Page "${page.title}" saved successfully`);
7925      
7926      // Double check that our gallery is still in the array
7927      const pageExists = galleries.some(g => g.id === id);
7928      console.log(`Page exists in galleries array: ${pageExists}`);
7929      
7930      if (pageExists) {
7931        // Load the new page after a slight delay to ensure DOM is updated
7932        setTimeout(() => {
7933          console.log(`Loading new page "${page.title}" with ID: ${id}`);
7934          loadPage(id);
7935        }, 50);
7936      } else {
7937        console.error(`Page with ID ${id} not found in galleries array after save`);
7938      }
7939    }).catch(error => {
7940      alert('Error saving page. Please try again.');
7941    });
7942
7943    return id;
7944  } catch (error) {
7945    console.error('Error adding page:', error);
7946    alert('Error adding page. Please try again.');
7947    return null;
7948  }
7949}
7950
7951/**
7952 * Ensures a slug is unique among existing galleries
7953 * @param {string} slug - The base slug to check
7954 * @param {Array} existingGalleries - Array of existing gallery configs
7955 * @returns {string} A unique slug
7956 */
7957function ensureUniqueSlug(slug, existingGalleries) {
7958  // Guard against invalid slugs
7959  if (!slug || slug === '-') {
7960    slug = 'page-' + Date.now();
7961  }
7962
7963  let finalSlug = slug;
7964  let counter = 1;
7965  
7966  // Check if slug already exists
7967  while (existingGalleries.some(g => g.slug === finalSlug)) {
7968    finalSlug = `${slug}-${counter}`;
7969    counter++;
7970  }
7971  
7972  return finalSlug;
7973}
7974
7975/**
7976 * Adds a new gallery based on the selected type
7977 * @returns {number|null} The new gallery ID or null if error
7978 */
7979function addGallery() {
7980  try {
7981    // Get the gallery title
7982    const titleInput = document.getElementById('galleryTitle');
7983    if (!titleInput) {
7984      console.error('Gallery title input not found');
7985      return;
7986    }
7987    const title = titleInput.value.trim();
7988    
7989    // Get the selected gallery type
7990    const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked');
7991    if (!galleryTypeRadio) {
7992      console.error('No gallery type selected');
7993      alert('Please select a gallery type');
7994      return;
7995    }
7996
7997    const galleryType = galleryTypeRadio.value;
7998    console.log("Selected gallery type:", galleryType);
7999
8000    // If this is a page, use the addPage function
8001    if (galleryType === 'page') {
8002      console.log("Creating page instead of gallery");
8003      return addPage();
8004    }
8005
8006    // For spacer type, we don't require a title
8007    if (galleryType !== 'spacer' && !title) {
8008      alert('Please enter a gallery title');
8009      return;
8010    }
8011
8012    // Continue with gallery creation logic
8013    // Generate unique ID
8014    const id = Date.now();
8015    
8016    // Get parent gallery if selected
8017    const parentSelect = document.getElementById('galleryParent');
8018    const parentId = parentSelect.value ? parseInt(parentSelect.value) : null;
8019    
8020    // Get URL if this is an external gallery
8021    const galleryUrl = document.getElementById('galleryUrl');
8022    const url = galleryTypeRadio.value === 'external' && galleryUrl 
8023      ? galleryUrl.value.trim() 
8024      : "";
8025    
8026    // Set basic properties
8027    const gallery = {
8028      id: id,
8029      title: title || "Spacer", // Use "Spacer" as internal title if no title provided
8030      parentId: parentId,
8031      visible: true
8032    };
8033    
8034    // Set type-specific properties
8035    if (galleryTypeRadio.value === 'spacer') {
8036      // Spacer properties
8037      gallery.isSpacer = true;
8038      gallery.title = title || "Spacer"; // For internal reference
8039    } else if (galleryTypeRadio.value === 'integrated') {
8040      // Generate pageId for integrated galleries
8041      gallery.isIntegrated = true;
8042      gallery.pageId = generatePageId();
8043      
8044      // Generate and ensure unique slug
8045      const baseSlug = slugify(title);
8046      gallery.slug = ensureUniqueSlug(baseSlug, galleries);
8047      gallery.url = "/" + gallery.slug;
8048      
8049      // Get the siteId from multiple possible sources
8050      const siteId = window.Parameters?.siteId || 
8051                    window.siteId || 
8052                    document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') ||
8053                    '';
8054      
8055      // Add galleryOptions with GUID=siteId as the manualCollectionName
8056      gallery.galleryOptions = {
8057        manualCollectionName: `GUID=${siteId}`,
8058        isIntegratedGallery: true,
8059        pageId: gallery.pageId,  // Store pageId for reference
8060        siteId: siteId
8061      };
8062    } else if (galleryTypeRadio.value === 'folder') {
8063      gallery.isFolder = true;
8064      gallery.isCollapsed = true; // Make folders closed by default
8065    } else if (galleryTypeRadio.value === 'external') {
8066      // External URL
8067      gallery.url = url;
8068      gallery.isExternal = true; // Add this flag to identify external URLs
8069    }
8070    
8071    console.log(`Creating new gallery with ID: ${id}, title: "${title}"`);
8072    
8073    // Add to galleries array
8074    galleries.push(gallery);
8075    
8076    // CRITICAL: Ensure window.galleries is synchronized
8077    if (typeof window.galleries === 'undefined') {
8078      window.galleries = galleries;
8079    } else {
8080      // Make sure the gallery is in window.galleries too
8081      const existingIndex = window.galleries.findIndex(g => g.id === id);
8082      if (existingIndex >= 0) {
8083        window.galleries[existingIndex] = gallery;
8084      } else {
8085        window.galleries.push(gallery);
8086      }
8087    }
8088    
8089    // Update UI first
8090    renderGalleries();
8091    clearAddForm();
8092    toggleAddForm();
8093    
8094    // Save galleries after UI update
8095    saveGalleries().then(() => {
8096      console.log(`Gallery "${gallery.title}" saved successfully`);
8097      
8098      // Double check that our gallery is still in the array
8099      const galleryExists = galleries.some(g => g.id === id);
8100      console.log(`Gallery exists in galleries array: ${galleryExists}`);
8101      
8102      // For non-folder types, load the new gallery content after a slight delay
8103      if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) {
8104        setTimeout(() => {
8105          console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`);
8106          if (gallery.isIntegrated) {
8107            loadGallery(id);
8108          } else if (gallery.isExternal && gallery.url) {
8109            // For external URLs, set the iframe src and mark active after navigation target is ready
8110            activeGalleryId = id;
8111            window.activeGalleryId = id;
8112            updateActiveStates();
8113            const frame = document.getElementById('galleryFrame');
8114            if (frame) frame.src = gallery.url;
8115          }
8116        }, 50);
8117      }
8118    }).catch(error => {
8119      console.error(`Error saving gallery "${gallery.title}":`, error);
8120      alert('Error saving gallery. Please try again.');
8121    });
8122    
8123    // Return the new gallery ID
8124    return id;
8125  } catch (error) {
8126    console.error('Error adding gallery:', error);
8127    alert('Error adding gallery. Please try again.');
8128  }
8129}
8130
8131function debugDOMState(message) {
8132console.log(`[DEBUG] ${message}`);
8133const galleryContainer = document.querySelector('.gallery-container');
8134const iframe = document.getElementById('galleryFrame');
8135const pageContainers = document.querySelectorAll('.page-container');
8136
8137console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`);
8138console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`);
8139console.log(`- Page containers: ${pageContainers.length} found`);
8140
8141pageContainers.forEach((container, index) => {
8142console.log(`  - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`);
8143});
8144}
8145
8146
8147/**
8148* Global helper: whether edit tools are explicitly toggled open.
8149* Do not use Parameters.isInEditor — that is also true for logged-in admins (cache bypass).
8150* Do not infer from sidebar/page .editing alone (can desync from the toggle).
8151*/
8152window.isInEditMode = function() {
8153  if (document.body.classList.contains('edit-mode-active')) {
8154    return true;
8155  }
8156  if (typeof window.isEditing === 'boolean') {
8157    return window.isEditing;
8158  }
8159  return false;
8160};
8161
8162/**
8163* This function synchronizes edit mode state across all components
8164*/
8165window.updateGlobalEditState = function(isEditing) {
8166console.log('Updating global edit state:', isEditing);
8167
8168// Update global variable
8169window.isEditing = isEditing;
8170
8171// Update Parameters object if it exists
8172if (window.Parameters) {
8173window.Parameters.isInEditor = isEditing;
8174}
8175
8176// Update sidebar class
8177const sidebar = document.querySelector('.sidebar');
8178if (sidebar) {
8179if (isEditing) {
8180  sidebar.classList.add('editing');
8181} else {
8182  sidebar.classList.remove('editing');
8183}
8184}
8185
8186// Update page containers
8187const pageContainers = document.querySelectorAll('.page-container');
8188pageContainers.forEach(container => {
8189if (isEditing) {
8190  container.classList.add('editing');
8191} else {
8192  container.classList.remove('editing');
8193}
8194});
8195
8196// Notify all components via event
8197document.dispatchEvent(new CustomEvent('edit-mode-changed', {
8198detail: { editing: isEditing }
8199}));
8200
8201if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') {
8202  window.syncContentMarginToHorizontalHeader();
8203}
8204
8205if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8206  window.syncGalleryOptionsButtonVisibility();
8207}
8208
8209// Gallery Options panel styling depends on body.edit-mode-active — hide it when leaving edit mode
8210if (!isEditing && typeof window.closeOptionsPanel === 'function') {
8211  window.closeOptionsPanel();
8212}
8213};
8214
8215/** Show or hide gallery options (#editorButton) based on edit mode and active content type */
8216window.syncGalleryOptionsButtonVisibility = function() {
8217  const btn = document.getElementById('editorButton');
8218  if (!btn) return;
8219
8220  const inEditMode = document.body.classList.contains('edit-mode-active');
8221  // When content type is explicitly gallery, ignore leftover .page-container nodes
8222  // (common when Help sandbox switches from a page to Example Gallery).
8223  const onPageView = window._activeContentType === 'gallery'
8224    ? false
8225    : (window._activeContentType === 'page' ||
8226      !!document.querySelector('.gallery-container .page-container'));
8227  let isActiveGallery = false;
8228  const activeId = window.activeGalleryId;
8229  const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null);
8230  const activeItem = activeId != null && list && typeof findGalleryById === 'function'
8231    ? findGalleryById(list, activeId)
8232    : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null);
8233  if (activeId != null && inEditMode && !onPageView) {
8234    isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal);
8235  }
8236
8237  const shouldShow = inEditMode && !onPageView && isActiveGallery;
8238  if (shouldShow) {
8239    if (window.Parameters) {
8240      window.Parameters.isInEditor = true;
8241    }
8242    btn.classList.remove('gallery-options-force-hidden');
8243    btn.style.removeProperty('display');
8244    btn.style.setProperty('display', 'flex', 'important');
8245    btn.style.pointerEvents = 'auto';
8246  } else {
8247    btn.classList.add('gallery-options-force-hidden');
8248    btn.style.setProperty('display', 'none', 'important');
8249    btn.style.pointerEvents = '';
8250  }
8251};
8252
8253document.addEventListener('gallery-loaded', function() {
8254  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8255    window.syncGalleryOptionsButtonVisibility();
8256  }
8257});
8258
8259document.addEventListener('page-loaded', function() {
8260  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8261    window.syncGalleryOptionsButtonVisibility();
8262  }
8263});
8264
8265/**
8266 * Wait for PageManager after CDN→proxy late script load (ISP/Firefox clients).
8267 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes.
8268 */
8269function whenPageManagerReady(callback, options) {
8270  const maxAttempts = (options && options.maxAttempts) || 50;
8271  const intervalMs = (options && options.intervalMs) || 100;
8272  let attempts = 0;
8273  const run = function() {
8274    if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8275      callback(window.PageManager);
8276      return;
8277    }
8278    attempts += 1;
8279    if (attempts >= maxAttempts) {
8280      console.error('❌ [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8281      if (options && typeof options.onTimeout === 'function') options.onTimeout();
8282      return;
8283    }
8284    if (attempts === 1 || attempts % 10 === 0) {
8285      console.warn('⏳ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts);
8286    }
8287    setTimeout(run, intervalMs);
8288  };
8289  run();
8290}
8291
8292/**
8293 * loadPage function that safely handles undefined galleries
8294 * @param {number} id - The gallery ID
8295 * @param {Event} event - Optional event object
8296 */
8297function loadPage(id, event){
8298  if (event && event.target && event.target.isConnected === false) {
8299    console.log('loadPage: Ignoring navigation from detached element');
8300    return;
8301  }
8302  if (event) {
8303    event.preventDefault();
8304    event.stopPropagation();
8305    const now = Date.now();
8306    const lastCallTime = window._lastPageLoadTime || 0;
8307    window._lastPageLoadTime = now;
8308    if (now - lastCallTime < 100) {
8309      console.log('Ignoring duplicate loadPage call');
8310      return;
8311    }
8312  }
8313  console.log('loadPage: Loading page with gallery ID:', id);
8314
8315  stopAutoAdvanceTimer();
8316  if (typeof dismissGalleryPasswordOverlay === 'function') {
8317    dismissGalleryPasswordOverlay();
8318  }
8319
8320  let galleriesData = window.galleries || galleries;
8321  const gallery = findGalleryById(galleriesData, id);
8322
8323  if (!gallery) {
8324    console.warn('No gallery found with ID:', id, 'for page load.');
8325    return;
8326  }
8327
8328  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
8329
8330  window.activeGalleryId = id;
8331  activeGalleryId = id;
8332  window._activeContentType = 'page';
8333  if (window.EnhancedGallerySystem) {
8334    window.EnhancedGallerySystem.currentGalleryId = null;
8335  }
8336
8337  if (typeof window.removeGalleryScriptsWithPause === 'function') {
8338    window.removeGalleryScriptsWithPause();
8339  }
8340
8341  const galleryContainer = document.querySelector('.gallery-container');
8342  if (galleryContainer) {
8343    galleryContainer.innerHTML = '';
8344  }
8345
8346  if (!gallery.pageId) gallery.pageId = `page_${id}`;
8347  if (!gallery.isPage) gallery.isPage = true;
8348
8349  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8350    window.syncGalleryOptionsButtonVisibility();
8351  }
8352
8353  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
8354  window._pageIdToGalleryId[gallery.pageId] = id;
8355
8356  if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
8357    if (window.PageManager && window.PageManager.elements) {
8358      window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8359    }
8360  } else if (window.PageManager && window.PageManager.elements &&
8361             window.PageManager.elements[gallery.pageId] &&
8362             window.PageManager.elements[gallery.pageId].length > 0) {
8363    gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
8364  }
8365
8366  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8367    try {
8368      window.PageManager.loadPage(gallery.pageId);
8369      const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
8370      if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
8371        window.PageManager.setEditMode(isCurrentlyEditing);
8372      }
8373    } catch (error) {
8374      console.error('Error loading page with PageManager:', error);
8375    }
8376  } else {
8377    console.error('PageManager not found yet — waiting for CDN proxy script');
8378    whenPageManagerReady(function() {
8379      try {
8380        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
8381          console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId);
8382          return;
8383        }
8384        if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) {
8385          window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8386        }
8387        window.PageManager.loadPage(gallery.pageId);
8388        console.log('loadPage: PageManager became available — page render retried');
8389      } catch (error) {
8390        console.error('Error loading page with PageManager after wait:', error);
8391      }
8392    });
8393  }
8394
8395  // Apply menu visibility
8396  const bodyEl = document.body;
8397  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
8398      bodyEl.classList.add('menu-hidden-on-page');
8399  } else {
8400      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
8401  }
8402  applyMenuOverlayClassForGallery(gallery);
8403
8404  if (typeof updateActiveStates === 'function') updateActiveStates();
8405  if (typeof updateMobileTitle === 'function') updateMobileTitle();
8406  if (typeof closeMobileMenu === 'function') closeMobileMenu();
8407  // Close the gallery options panel when navigating to a different page
8408  if (typeof window.closeOptionsPanel === 'function') {
8409    window.closeOptionsPanel();
8410  }
8411  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
8412  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8413    window.syncGalleryOptionsButtonVisibility();
8414  }
8415  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
8416    window.dispatchEvent(new CustomEvent('page-loaded', {
8417      detail: { pageId: gallery.pageId, galleryId: id }
8418    }));
8419  }
8420}
8421
8422
8423
8424
8425
8426function generatePageId() {
8427const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
8428let result = '';
8429const charactersLength = characters.length;
8430for (let i = 0; i < 8; i++) {
8431  result += characters.charAt(Math.floor(Math.random() * charactersLength));
8432}
8433return result;
8434};
8435
8436
8437function normalizeNameForComparison(name) {
8438if (!name) {
8439console.log("Empty name passed to normalization");
8440return '';
8441}
8442
8443console.log(`Normalizing name: "${name}"`);
8444
8445// Step 1: Convert to lowercase
8446let normalized = name.toLowerCase();
8447console.log(`After lowercase: "${normalized}"`);
8448
8449// Step 2: Replace spaces with hyphens (without regex)
8450normalized = normalized.split(' ').join('-');
8451console.log(`After space replacement: "${normalized}"`);
8452
8453// Step 3: Filter out unwanted characters (without regex)
8454let filtered = '';
8455for (let i = 0; i < normalized.length; i++) {
8456const char = normalized.charAt(i);
8457// Keep only a-z, 0-9, and hyphens
8458if ((char >= 'a' && char <= 'z') || 
8459    (char >= '0' && char <= '9') || 
8460    char === '-') {
8461  filtered += char;
8462}
8463}
8464normalized = filtered;
8465console.log(`After character filtering: "${normalized}"`);
8466
8467// Step 4: Clean up multiple hyphens (without regex)
8468while (normalized.indexOf('--') !== -1) {
8469normalized = normalized.split('--').join('-');
8470}
8471
8472// Step 5: Remove leading and trailing hyphens (without regex)
8473if (normalized.charAt(0) === '-') {
8474normalized = normalized.substring(1);
8475}
8476if (normalized.charAt(normalized.length - 1) === '-') {
8477normalized = normalized.substring(0, normalized.length - 1);
8478}
8479
8480// Check if the result is valid
8481if (!normalized || normalized === '-') {
8482console.log(`WARNING: Normalization produced invalid result: "${normalized}"`);
8483} else {
8484console.log(`Final normalized slug: "${normalized}"`);
8485}
8486
8487return normalized;
8488}
8489
8490function ensureUniqueSlug(slug, existingGalleries) {
8491if (!slug) return 'gallery';
8492
8493let finalSlug = slug;
8494let counter = 1;
8495
8496// Check if slug exists in any gallery
8497const slugExists = function(s) {
8498for (let i = 0; i < existingGalleries.length; i++) {
8499  if (existingGalleries[i].slug === s) {
8500    return true;
8501  }
8502}
8503return false;
8504};
8505
8506// Keep incrementing counter until unique
8507while (slugExists(finalSlug)) {
8508finalSlug = slug + '-' + counter;
8509counter++;
8510}
8511
8512return finalSlug;
8513}
8514
8515/**
8516 * Gathers the current complete site configuration from the client-side state
8517 * and triggers a download of the data as a single JSON file.
8518 * This provides a simple way to back up or export the current site structure.
8519 */
8520function downloadCurrentSiteJSON() {
8521  try {
8522    console.log('Gathering current site data for download...');
8523
8524    // Get the current hostname to use in the filename.
8525    const hostname = window.location.hostname.replace('www.', '');
8526
8527    // --- FIX: Ensure we get the most up-to-date menu styles ---
8528    // The MenuStyleCustomizer.settings object is the most reliable source
8529    // for the current styles being used on the client.
8530    const currentMenuStyles = window.MenuStyleCustomizer?.settings || {};
8531
8532    // Get current galleries
8533    const currentGalleries = window.galleries || galleries || [];
8534
8535    // --- STREAMLINED: Process galleries without including image data ---
8536    const enhancedGalleries = currentGalleries.map(gallery => {
8537      // If this is a Classic collection gallery, add note about manual retrieval
8538      if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) {
8539        const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5);
8540        console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`);
8541        
8542        return {
8543          ...gallery,
8544          _note: `This gallery references Classic collection GUID=${classicGuid}. To get the full image data with captions, you may need to manually fetch the Classic collection JSON from: https://storage.neonsky.app/sites:site_${classicGuid}.json`,
8545          _classicGuid: classicGuid,
8546          _requiresClassicData: true
8547        };
8548      }
8549      
8550      return gallery;
8551    });
8552
8553    // Assemble the complete site configuration object from global state variables.
8554    // This structure mirrors what the `save-config` API endpoint expects, ensuring
8555    // the exported file is a complete and re-importable replica.
8556    const siteData = {
8557      // The core menu/page structure with enhanced Classic collection data.
8558      galleries: enhancedGalleries,
8559      
8560      // Admin emails are managed server-side for security. We export the list if it's
8561      // available on the client from a previous check, but the server remains the
8562      // source of truth. A re-import will not overwrite the admin list.
8563      adminEmails: window.siteConfig?.adminEmails || [],
8564      
8565      // The unique ID for the site.
8566      siteId: window.siteId || window.Parameters?.siteId || '',
8567      
8568      // Custom styling rules for the menu.
8569      menuStyles: currentMenuStyles,
8570      
8571      // All elements configured to appear in the sidebar.
8572      sidebarElements: window.SidebarManager?.elements || [],
8573      
8574      // Global site metadata for SEO and analytics.
8575      siteMetadata: window.siteMetadata || {},
8576      
8577      // Add metadata about Classic collections that need manual data retrieval
8578      _classicCollections: enhancedGalleries
8579        .filter(g => g._requiresClassicData)
8580        .map(g => ({
8581          title: g.title,
8582          guid: g._classicGuid,
8583          note: g._note
8584        }))
8585    };
8586
8587    console.log('Enhanced site data collected for export:', siteData);
8588
8589    // Convert the JavaScript object to a formatted JSON string.
8590    const jsonString = JSON.stringify(siteData, null, 2);
8591
8592    // Create a Blob, which is a file-like object of immutable, raw data.
8593    const blob = new Blob([jsonString], { type: 'application/json' });
8594
8595    // Create a temporary link element to trigger the browser's download functionality.
8596    const url = URL.createObjectURL(blob);
8597    const a = document.createElement('a');
8598    a.href = url;
8599    a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json'
8600    document.body.appendChild(a);
8601    a.click();
8602
8603    // Clean up by removing the temporary link and revoking the object URL to free up memory.
8604    document.body.removeChild(a);
8605    URL.revokeObjectURL(url);
8606
8607    console.log('Enhanced JSON download initiated successfully.');
8608
8609    // Show a helpful message about Classic collections
8610    const classicCount = siteData._classicCollections?.length || 0;
8611    
8612    if (classicCount > 0) {
8613      console.log(`Export Summary: ${classicCount} Classic collection(s) detected. The exported JSON includes gallery structure and settings, but for full image data with captions, you may 
8613need to manually fetch the Classic collection JSON files.`);
8614    }
8615
8616  } catch (error) {
8617    console.error('Error creating JSON download:', error);
8618    alert('An error occurred while preparing the download. Please check the console.');
8619  }
8620}
8621
8622// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array)
8623function buildHydraClassicNdjson(classicGuid, images) {
8624  const header = {
8625    title: 'Untitled',
8626    description: '',
8627    version: '1',
8628    isClassic: true,
8629    isClassicCollection: true,
8630    classicGuid: 'GUID=' + String(classicGuid || '')
8631  };
8632
8633  const imageArray = Array.isArray(images) ? images.map(function(img, idx) {
8634    var path = '';
8635    if (img) {
8636      path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || '';
8637    }
8638    // Derive filename base from any provided path/filename and normalize extension to .jpg
8639    var fnameBase = '';
8640    if (typeof path === 'string' && path.length > 0) {
8641      var slash = path.lastIndexOf('/');
8642      var raw = slash >= 0 ? path.substring(slash + 1) : path;
8643      // remove query/hash
8644      var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q);
8645      var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h);
8646      // strip extension if present
8647      var dot = raw.lastIndexOf('.');
8648      fnameBase = dot > 0 ? raw.substring(0, dot) : raw;
8649    } else if (img && typeof img.filename === 'string') {
8650      var rf = img.filename;
8651      var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2);
8652      var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2);
8653      var d2 = rf.lastIndexOf('.');
8654      fnameBase = d2 > 0 ? rf.substring(0, d2) : rf;
8655    }
8656    var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.'))
8657      ? img.metadata.extension : '.jpg';
8658    var filename = fnameBase ? (fnameBase + ext) : '';
8659    // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename}
8660    // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path
8661    var guidRaw = String(classicGuid || '').replace(/^GUID=/, '');
8662    var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path;
8663    var rec = {
8664      caption: (img && (img.caption || img.description)) || '',
8665      description: (img && (img.description || img.caption)) || '',
8666      isPreview: idx === 1,
8667      title: (img && img.title) || '',
8668      'alt-text': (img && (img['alt-text'] || img.altText || '')) || '',
8669      image: cdnImageUrl || '',
8670      link: '',
8671      'link-text': '',
8672      filename: filename,
8673      isClassic: true,
8674      isClassicCollection: true,
8675      classicGuid: 'GUID=' + String(classicGuid || '')
8676    };
8677    return rec;
8678  }) : [];
8679
8680  // Build NDJSON as: header line, imageMetadata array line, then one line per full image record
8681  var body = { imageMetadata: imageArray };
8682  var out = [JSON.stringify(header), JSON.stringify(body)];
8683  for (var k = 0; k < imageArray.length; k++) {
8684    out.push(JSON.stringify(imageArray[k]));
8685  }
8686  return out.join('\n') + '\n';
8687}
8688
8689async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) {
8690  // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB)
8691  const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson';
8692  const flyUrl = 'https://hydra-press-v2.fly.dev/publish';
8693  const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys';
8694  
8695  console.error('Starting dual NDJSON upload for:', fileName);
8696  console.error('Content length:', ndjsonContent.length, 'characters');
8697  
8698  // COMPRESSION: Compress for Irys only (not Tigris)
8699  console.error('🔄 Compressing NDJSON with gzip for Irys...');
8700  const encoder = new TextEncoder();
8701  const ndjsonBytes = encoder.encode(ndjsonContent);
8702  const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip'));
8703  const compressedBlob = await new Response(compressedStream).blob();
8704  const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer());
8705  
8706  const originalSize = ndjsonBytes.length;
8707  const compressedSize = compressedBytes.length;
8708  const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
8709  
8710  console.error('📊 Compression results:', {
8711    original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)',
8712    compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)',
8713    ratio: compressionRatio + '%'
8714  });
8715  
8716  const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload
8717  const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT;
8718  
8719  console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)');
8720  
8721  // STEP 1: Upload to Irys (non-blocking, compressed if <800KB)
8722  let irysResult = null;
8723  if (shouldUploadToIrys) {
8724    try {
8725      console.error('🔄 Starting Irys upload (compressed, non-blocking)...');
8726      
8727      // Convert compressed bytes to base64
8728      const compressedBase64 = btoa(String.fromCharCode(...compressedBytes));
8729      
8730      const irysResp = await fetch(irysUrl, {
8731        method: 'POST',
8732        headers: { 'content-type': 'application/json' },
8733        body: JSON.stringify({ 
8734          compressedData: compressedBase64,
8735          originalSize: originalSize,
8736          compressedSize: compressedSize,
8737          siteId: siteId,
8738          pageId: pageId
8739        })
8740      });
8741      
8742      if (irysResp.ok) {
8743        irysResult = await irysResp.json();
8744        console.error('✅ Irys upload successful:', irysResult.txId);
8745        console.error('   Irys Gateway:', irysResult.urls?.irys);
8746        console.error('   ar.io Gateway:', irysResult.urls?.arweave);
8747        
8748        // Log wallet balance and cost
8749        if (irysResult.walletBalance !== undefined) {
8750          console.error('💰 Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR');
8751        }
8752        if (irysResult.uploadCost !== undefined) {
8753          const costUSD = (irysResult.uploadCost * 20).toFixed(4);
8754          if (irysResult.uploadCost === 0) {
8755            console.error('💸 Upload Cost: FREE (0 AR)');
8756          } else {
8757            console.error('💸 Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)');
8758          }
8759        }
8760      } else {
8761        const errorText = await irysResp.text().catch(() => 'Unknown error');
8762        console.warn('⚠️ Irys upload failed (non-critical):', irysResp.status, errorText);
8763      }
8764    } catch (irysError) {
8765      console.warn('⚠️ Irys upload error (non-critical):', irysError.message);
8766      // Continue - Irys failure doesn't block Fly.io upload
8767    }
8768  } else {
8769    console.error('⏭️ Skipping Irys upload (compressed size exceeds 800KB limit)');
8770  }
8771  
8772  // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed)
8773  // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher)
8774  try {
8775    console.error('🔄 Starting Fly.io upload (uncompressed)...');
8776    const flyHeaders = { 'content-type': 'application/json' };
8777    let authToken = null;
8778    try {
8779      if (window.TokenManager) {
8780        if (typeof window.TokenManager.getHydraAuthToken === 'function') {
8781          try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
8782        }
8783        if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
8784          try {
8785            const tw = window.TokenManager.getBestToken();
8786            authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw;
8787          } catch (e) {}
8788        }
8789      }
8790      if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
8791      if (!authToken) authToken = window.authToken;
8792      if (authToken) {
8793        flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
8794          ? 'Bearer ' + authToken
8795          : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
8796        console.error('🔐 Fly.io auth: token attached');
8797      } else {
8798        console.warn('⚠️ No auth token - Fly.io may reject Hydra site upload (401)');
8799      }
8800    } catch (authErr) {
8801      console.warn('⚠️ Auth token lookup failed:', authErr.message);
8802    }
8803    const flyResp = await fetch(flyUrl, {
8804      method: 'POST',
8805      headers: flyHeaders,
8806      body: JSON.stringify({ ndjsonContent, dataFileName: fileName })
8807    });
8808    
8809    console.log('Server response status:', flyResp.status);
8810    
8811    if (!flyResp.ok) {
8812      const errorText = await flyResp.text().catch(() => 'Unknown error');
8813      console.error('❌ Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText);
8814      if (flyResp.status === 401) {
8815        console.error('💡 Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.');
8816      }
8817      return { success: false, irysResult: null };
8818    }
8819    
8820    const flyData = await flyResp.json().catch(() => null);
8821    if (flyData && flyData.urls) {
8822      console.log('✅ Fly.io upload successful:', flyData.urls);
8823      console.log('   Tigris URL:', flyData.urls.urlTigris);
8824      
8825      // Return comprehensive result
8826      const result = {
8827        success: true,
8828        flyUrls: flyData.urls,
8829        irysResult: irysResult,
8830        uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'],
8831        fileSize: compressedSize, // Irys compressed size
8832        originalFileSize: originalSize // Original uncompressed size
8833      };
8834      
8835      console.log('📦 Upload complete:', {
8836        destinations: result.uploadedTo.join(' + '),
8837        txId: irysResult?.txId || 'N/A',
8838        flyUrl: flyData.urls.urlTigris
8839      });
8840      
8841      return result;
8842    } else {
8843      console.warn('Fly.io upload response missing URLs:', flyData);
8844      return { success: false, irysResult: null };
8845    }
8846  } catch (flyError) {
8847    console.error('❌ Fly.io upload error for', fileName + ':', flyError);
8848    return { success: false, irysResult: null };
8849  }
8850}
8851
8852async function importClassicGalleries() {
8853  // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS
8854  function cleanDescriptionLeadingLines(text) {
8855    if (!text || typeof text !== 'string') return text;
8856
8857    let result = text;
8858
8859    // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT>
8860    result = result.replace(
8861      new RegExp(
8862        '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+',
8863        'i'
8864      ),
8865      ''
8866    );
8867
8868    // Remove leading empty P tags
8869    result = result.replace(
8870      new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'),
8871      ''
8872    );
8873
8874    // Remove leading empty FONT tags
8875    result = result.replace(
8876      new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'),
8877      ''
8878    );
8879
8880    // Remove leading BR tags
8881    result = result.replace(
8882      new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'),
8883      ''
8884    );
8885
8886    // Remove any remaining leading whitespace
8887    return result.replace(new RegExp('^\s+', 'i'), '').trim();
8888  }
8889
8890  // Browser-scoped helper: fix multi-line links (template-safe escaped regex)
8891  function fixMultilineLinks(text) {
8892    if (!text || typeof text !== 'string') return text;
8893    return text.replace(
8894      new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'),
8895      function (match) {
8896        const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i'));
8897        if (first) {
8898          return '<a href="' + first[1] + '" target="' + first[2] + '">
8898Click HERE to Add to Cart</a>';
8899        }
8900        return match;
8901      }
8902    );
8903  }
8904
8905  // Browser-scoped helper: convert plain text link phrase to HTML link
8906  function convertPlainTextLinks(text) {
8907    if (!text || typeof text !== 'string') return text;
8908    return text.replace(
8909      new RegExp('Click HERE to Add to Cart', 'g'),
8910      '<a href="#" target="_blank">Click HERE to Add to Cart</a>'
8911    );
8912  }
8913  // Get input elements, including the new checkbox
8914  const guidInputEl = document.getElementById('classicGuid');
8915  const pastedJsonEl = document.getElementById('pastedJson');
8916  const parentIdInputEl = document.getElementById('importParent');
8917  const createSubmenuCheckboxEl = document.getElementById('createSubmenu');
8918  const submenuTitleInputEl = document.getElementById('submenuTitle');
8919  const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox');
8920
8921  // Get values from inputs
8922  const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : '';
8923  const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : '';
8924  const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false;
8925  
8926  let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null;
8927  let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false;
8928  let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : '';
8929
8930  // If replacing the menu, ignore parent/submenu settings
8931  if (replaceMenu) {
8932      parentId = null;
8933      createSubmenu = false;
8934  }
8935
8936  // Validate submenu title if checkbox is checked
8937  if (createSubmenu && !submenuTitle) {
8938    alert('Please provide a title for the submenu');
8939    return;
8940  }
8941
8942  // Show status display
8943  const statusEl = document.getElementById('importStatus');
8944  const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null;
8945  const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null;
8946  const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null;
8947  const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null;
8948
8949  if (statusEl && progressBar && statusMessage) {
8950    statusEl.style.display = 'block';
8951    statusEl.classList.add('importing');
8952    progressBar.style.width = '10%';
8953    statusMessage.textContent = 'Preparing to import...';
8954  }
8955
8956  try {
8957    let classicData;
8958    let classicGuid; // Will hold the primary GUID for image path lookups
8959
8960    // --- LOGIC TO GET DATA: Prioritize Pasted JSON ---
8961    if (pastedJson) {
8962      if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...';
8963      classicData = JSON.parse(pastedJson);
8964
8965      if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
8966        throw new Error('Pasted JSON is invalid: "galleries" array not found.');
8967      }
8968      // Use the first available GUID from the pasted data as the default for images
8969      classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId;
8970
8971    } else if (guidInput) {
8972      // Check if this is the new import_ format
8973      if (guidInput.startsWith('import_')) {
8974        if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...';
8975        const importGuid = guidInput.substring(7); // Remove 'import_' prefix
8976        
8977        const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`;
8978        const response = await fetch(jsonUrl);
8979        if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
8980        
8981        classicData = await response.json();
8982        
8983        if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
8984          throw new Error('Fetched import JSON is invalid: "galleries" array not found.');
8985        }
8986        
8987        // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata
8988        if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...';
8989        
8990        const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`;
8991        
8992        try {
8993          const originalResponse = await fetch(originalJsonUrl);
8994          if (originalResponse.ok) {
8995            const originalData = await originalResponse.json();
8996            
8997            // Get the default language from site configuration
8998            let defaultLanguageID = 'en'; // fallback
8999            if (originalData.languages && Array.isArray(originalData.languages)) {
9000              const defaultLang = originalData.languages.find(lang => lang.isDefault === true);
9001              if (defaultLang) {
9002                defaultLanguageID = defaultLang.id;
9003              }
9004            }
9005            
9006            // Merge image metadata from original JSON into import JSON galleries
9007            if (originalData.galleries && Array.isArray(originalData.galleries)) {
9008              classicData.galleries = classicData.galleries.map(importGallery => {
9009                // Find matching gallery in original data by name or categoryID
9010                
9011                const originalGallery = originalData.galleries.find(orig => 
9012                  orig.title === importGallery.title || // Primary match by title
9013                  orig.name === importGallery.title ||  // Fallback: orig.name matches import.title
9014                  orig.categoryID === importGallery.categoryID // Fallback: categoryID match
9015                );
9016                
9017                
9018                if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) {
9019                  
9020                  // Process all images from original gallery with metadata extraction
9021                    const processedImages = originalGallery.images.map(originalImage => {
9022                    // Apply EXACT same metadata processing logic as Edition Publisher
9023                    let title = originalImage.title || '';
9024                    let dateline = originalImage.dateline || '';
9025                    let caption = originalImage.caption || '';
9026                    
9027                    // Check languageVersions array for metadata (this is where most data is stored)
9028                    if (originalImage.languageVersions && originalImage.languageVersions.length > 0) {
9029                      // Look for site's default language first, then fall back to first available
9030                      let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID);
9031                      if (!langData) {
9032                        langData = originalImage.languageVersions[0];
9033                      }
9034                      
9035                      // For language-enabled JSON files, prioritize languageVersions data
9036                      // Only fall back to top-level if languageVersions data is empty
9037                      if (!title || title.trim() === '') title = langData.title || '';
9038                      if (!dateline || dateline.trim() === '') dateline = langData.dateline || '';
9039                      if (!caption || caption.trim() === '') caption = langData.caption || '';
9040                    }
9041                    
9042                    // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals)
9043                    let description = caption;
9044                    if (caption && caption.includes('TEXTFORMAT')) {
9045                      try {
9046                        // Use properly escaped HTML characters for template literal context
9047                        description = caption
9048                          .replace(/\<TEXTFORMAT[^\>]*\>/g, '')
9049                          .replace(/\<\/TEXTFORMAT\>/g, '')
9050                          .replace(/\<P[^\>]*\>/g, '\<p\>')
9051                          .replace(/\<\/P\>/g, '\<\/p\>')
9052                          .replace(/\<FONT[^\>]*\>/g, '')
9053                          .replace(/\<\/FONT\>/g, '')
9054                          .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>')
9055                          .replace(/\<\/A\>/g, '\<\/a\>')
9056                          .replace(/\<U\>/g, '\<u\>')
9057                          .replace(/\<\/U\>/g, '\<\/u\>')
9058                          .replace(/\<B\>/g, '\<strong\>')
9059                          .replace(/\<\/B\>/g, '\<\/strong\>')
9060                          .replace(/\<I\>/g, '\<em\>')
9061                          .replace(/\<\/I\>/g, '\<\/em\>')
9062                          .replace(/&quot;/g, '"')
9063                          .replace(/&apos;/g, "'")
9064                          .replace(/\s+/g, ' ')
9065                          .trim();
9066                      } catch (error) {
9067                        console.warn('TEXTFORMAT conversion failed, using original caption:', error);
9068                        description = caption;
9069                      }
9070                    }
9071                    
9072                    // Prepend dateline to description if it exists (convert to uppercase)
9073                    if (dateline) {
9074                      const uppercaseDateline = dateline.toUpperCase();
9075                      description = `${uppercaseDateline} ${description}`.trim();
9076                    }
9077                    
9078                    // Apply description fixes
9079                    description = cleanDescriptionLeadingLines(description);
9080                    description = fixMultilineLinks(description);
9081                    description = convertPlainTextLinks(description);
9082                    
9083                  const imageEntry = {
9084                        // Match EXACT structure from Edition Publisher hydraImageEntry
9085                        image: originalImage.image || originalImage.urls?.original || '',
9086                        caption: description, // Use processed description as caption
9087                        description: description, // ALSO set description field (like Edition Publisher)
9088                        title: title,
9089                        'alt-text': title, // Use title as alt-text like Edition Publisher
9090                        link: originalImage.urls?.original || '',
9091                        'link-text': originalImage.urls?.original || '',
9092                        isPreview: false,
9093                        filename: originalImage.filename || '',
9094                        originalUrl: originalImage.image || originalImage.urls?.original || '',
9095                        dateline: dateline
9096                  };
9097
9098                  return imageEntry;
9099                  });
9100                  
9101                  return {
9102                    ...importGallery, // Keep import gallery structure (order, settings, etc.)
9103                    images: processedImages // Replace with processed original images
9104                  };
9105                }
9106                return importGallery;
9107              });
9108            }
9109          } else {
9110            console.warn('Could not fetch original JSON for metadata, using import data only');
9111          }
9112        } catch (error) {
9113          console.warn('Error fetching original JSON for metadata:', error);
9114        }
9115        
9116        // Extract original GUID for image path lookups
9117        // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID
9118        // GUIDs are typically 13 characters, so split on common separators
9119        let baseGuid = importGuid;
9120        const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i);
9121        if (guidMatch) {
9122          baseGuid = guidMatch[1];
9123        } else {
9124          // Fallback: try to extract first 13-character hex string
9125          const hexMatch = importGuid.match(/^([a-f0-9]{13})/i);
9126          if (hexMatch) {
9127            baseGuid = hexMatch[1];
9128          }
9129        }
9130        
9131        // Use the base GUID for image path lookups (e.g., /images/filename.jpg)
9132        classicGuid = baseGuid;
9133        console.log('Successfully loaded hybrid import data with import GUID:', importGuid);
9134        console.log('Using base GUID for image paths:', classicGuid);
9135        
9136             } else {
9137         // Standard GUID format - existing logic
9138         if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9139         classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput;
9140         console.log('Detected standard GUID format, GUID:', classicGuid);
9141         
9142         const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`;
9143         console.log('Fetching classic JSON from:', jsonUrl);
9144         const response = await fetch(jsonUrl);
9145
9146         if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9147         classicData = await response.json();
9148         
9149         if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9150           throw new Error('Fetched JSON is invalid: "galleries" array not found.');
9151         }
9152         
9153         console.log('Successfully loaded classic data with GUID:', classicGuid);
9154       }
9155
9156    } else {
9157      alert('Please either paste site JSON or enter a Classic GUID to import.');
9158      if (statusEl) statusEl.style.display = 'none';
9159      return;
9160    }
9161    
9162    const galleriesForImport = classicData.galleries;
9163    if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`;
9164
9165    // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries
9166    const isImportFormat = !!(guidInput && guidInput.startsWith('import_'));
9167
9168    // --- HIERARCHY RECONSTRUCTION LOGIC ---
9169    
9170    const currentSiteId = window.Parameters?.siteId || siteId;
9171    let submenuParentId = parentId;
9172    const tempNewItems = [];
9173    const originalIdToNewIdMap = {};
9174
9175    // Create the optional top-level submenu if requested
9176    if (createSubmenu) {
9177        const submenuItem = {
9178            id: Date.now(),
9179            title: submenuTitle,
9180            isSubmenu: true,
9181            isCollapsed: true,
9182            visible: true,
9183            parentId: parentId,
9184            siteId: currentSiteId,
9185            children: []
9186        };
9187        galleries.push(submenuItem);
9188        submenuParentId = submenuItem.id;
9189    }
9190
9191    // --- Pass 1: Create new items with correct data and map old IDs to new IDs ---
9192    for (let index = 0; index < galleriesForImport.length; index++) {
9193        const classicItem = galleriesForImport[index];
9194        const newItemId = Date.now() + index + 100;
9195        originalIdToNewIdMap[classicItem.id] = newItemId;
9196
9197        if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`;
9198        if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`;
9199
9200        const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`;
9201        
9202 let slug;
9203        // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean.
9204        // Otherwise (for GUID import), trust the existing slug if it's there.
9205        if (pastedJson) {
9206            slug = normalizeNameForComparison(title);
9207        } else if (classicItem.slug && classicItem.slug.trim() !== '') {
9208            slug = classicItem.slug;
9209        } else {
9210            slug = normalizeNameForComparison(title);
9211        }
9212const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness
9213
9214        // Determine the GUID to use for this specific item's image paths
9215        const itemGuid = classicItem.classicGuid || classicGuid;
9216
9217     let newItem;
9218
9219        // --- Check if it's an External URL ---
9220        if (classicItem.isExternal === true && classicItem.url) {
9221            console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`);
9222            newItem = {
9223                id: newItemId, 
9224                title, 
9225                url: classicItem.url, 
9226                isExternal: true, 
9227                visible: classicItem.visible !== false, 
9228                parentId: null,
9229                siteId: currentSiteId, 
9230                slug: finalSlug,
9231                importSource: 'external-url'
9232            };
9233        }
9234        // --- Check if it's a Page and build its elements ---
9235        else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) {
9236            const pageUniqueId = `page_${newItemId}`;
9237            
9238            let pageElements = [];
9239            
9240            // =================================================================
9241            // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format
9242            // =================================================================
9243            if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) {
9244                console.log(`Using existing pageElements for page: "${title}"`);
9245                
9246                // Check if we have both pageElements and images - create two-column layout
9247                if (classicItem.images && classicItem.images.length > 0) {
9248                    console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`);
9249                    
9250                    // Create metadata element
9251                    pageElements.push({
9252                        id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9253                        metaTitle: classicItem.meta?.title || title,
9254                        metaDescription: classicItem.meta?.description || "",
9255                        metaKeywords: classicItem.meta?.keywords || ""
9256                    });
9257                    
9258                    // Create two-column container
9259                    const leftColumnElements = [];
9260                    const rightColumnElements = [];
9261                    
9262                    // Add images to left column
9263                    const slides = classicItem.images.map((img) => {
9264                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9265                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9266                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9267                        
9268                        return { 
9269                            imageUrl, 
9270                            imageKey, 
9271                            imageFilename, 
9272                            imageType: img.metadata?.mimetype || 'image/jpeg',
9273                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9274                            title: img.title || '',
9275                            byline: img.byline || '',
9276                            dateline: img.dateline || '',
9277                            altText: img.metadata?.alt_text || ''
9278                        };
9279                    });
9280                    
9281                    leftColumnElements.push({
9282                        id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0,
9283                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9284                    });
9285                    
9286                    // Add text content to right column
9287                    classicItem.pageElements.forEach((el, elIndex) => {
9288                        if (el.type === 'text' && el.content) {
9289                            rightColumnElements.push({
9290                                id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex,
9291                                textContent: el.content, textWidth: 70
9292                            });
9293                        }
9294                    });
9295                    
9296                    const columns = [
9297                        { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements },
9298                        { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }
9299                    ];
9300                    
9301                    pageElements.push({
9302                        id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1,
9303                        columns,
9304                        backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9305                        backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9306                    });
9307                    
9308                } else {
9309                    // No images, just transform the pageElements as before
9310                    pageElements = classicItem.pageElements.map((el, elIndex) => {
9311                        const elementId = Date.now() + index + 1000 + elIndex;
9312                        
9313                        // Handle import_guid.json format (type + content)
9314                        if (el.type && el.content) {
9315                            if (el.type === 'text') {
9316                                return {
9317                                    id: elementId,
9318                                    type: 'text',
9319                                    title: 'Text Block',
9320                                    visible: true,
9321                                    position: elIndex,
9322                                    textContent: el.content,
9323                                    textWidth: 100
9324                                };
9325                            } else if (el.type === 'metadata') {
9326                                return {
9327                                    id: elementId,
9328                                    type: 'metadata',
9329                                    title: 'Metadata',
9330                                    visible: true,
9331                                    position: elIndex,
9332                                    metaTitle: el.metaTitle || title,
9333                                    metaDescription: el.metaDescription || '',
9334                                    metaKeywords: el.metaKeywords || ''
9335                                };
9336                            }
9337                        }
9338                        
9339                        // Handle existing format (already has id, position, etc.)
9340                        return {
9341                            ...el,
9342                            id: elementId,
9343                            position: el.position !== undefined ? el.position : elIndex
9344                        };
9345                    });
9346                }
9347
9348            } else {
9349                // Otherwise (for GUID-based import), generate the page structure from scratch.
9350                console.log(`Generating new pageElements for page: "${title}"`);
9351                pageElements.push({
9352                  id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9353                  metaTitle: classicItem.meta?.title || title,
9354                  metaDescription: classicItem.meta?.description || "",
9355                  metaKeywords: classicItem.meta?.keywords || ""
9356                });
9357
9358                // Add images if they exist in the page
9359                if (classicItem.images && classicItem.images.length > 0) {
9360                    const slides = classicItem.images.map((img) => {
9361                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9362                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9363                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9364                        
9365                        return { 
9366                            imageUrl, 
9367                            imageKey, 
9368                            imageFilename, 
9369                            imageType: img.metadata?.mimetype || 'image/jpeg',
9370                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9371                            title: img.title || '',
9372                            byline: img.byline || '',
9373                            dateline: img.dateline || '',
9374                            altText: img.metadata?.alt_text || ''
9375                        };
9376                    });
9377                    
9378                    pageElements.push({
9379                        id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1,
9380                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true
9381                    });
9382                }
9383
9384                pageElements.push({
9385                  id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2,
9386                  textContent: "<p>&nbsp;</p><p><br></p>", textWidth: 100
9387                });
9388
9389                const columns = [];
9390                const leftColumnElements = [];
9391                const rightColumnElements = [];
9392
9393                if (classicItem.images && classicItem.images.length > 0) {
9394                  const slides = classicItem.images.map((img) => {
9395                    const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9396                    const imageKey = `${itemGuid}/images/${imageFilename}`;
9397                    const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9398                    
9399                    // Preserve all image metadata including captions, titles, etc.
9400                    return { 
9401                      imageUrl, 
9402                      imageKey, 
9403                      imageFilename, 
9404                      imageType: img.metadata?.mimetype || 'image/jpeg',
9405                      // Preserve caption information - handle both direct and language version storage patterns
9406                      caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))),
9407                      title: img.title || img.languageVersions?.[0]?.title || '',
9408                      byline: img.byline || img.languageVersions?.[0]?.byline || '',
9409                      dateline: img.dateline || img.languageVersions?.[0]?.dateline || '',
9410                      altText: img.metadata?.alt_text || '',
9411                      // Preserve any additional metadata
9412                      metaTitle: img.metadata?.meta_title || '',
9413                      metaDescription: img.metadata?.meta_description || '',
9414                      metaKeywords: img.metadata?.meta_keywords || '',
9415                      // Preserve gallery-specific data
9416                      sortOrder: img.galleryData?.sortOrder || 0,
9417                      linkURL: img.galleryData?.linkURL || null,
9418                      linkTarget: img.galleryData?.linkTarget || '_self'
9419                    };
9420                  });
9421                  leftColumnElements.push({
9422                    id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0,
9423                    slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9424                  });
9425                }
9426
9427                let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || "";
9428                if (rawDetails && typeof rawDetails === 'string') {
9429                    // =================================================================
9430                    // CHANGE 2: Fix for "Invalid regular expression flags" error.
9431                    // Changed from /.../gi literal to new RegExp('...', 'gi') constructor
9432                    // to prevent syntax errors.
9433                    // =================================================================
9434                    let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), '');
9435                    processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>');
9436                    processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), '');
9437                    processedText = processedText.replace(new RegExp('<B>
9437', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>');
9438                    processedText = processedText.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
9439                    
9440                    // Apply description fixes to processed text
9441                    processedText = cleanDescriptionLeadingLines(processedText);
9442                    processedText = fixMultilineLinks(processedText);
9443                    processedText = convertPlainTextLinks(processedText);
9444                    
9445                    rightColumnElements.push({
9446                      id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0,
9447                      textContent: processedText, textWidth: 70
9448                    });
9449                }
9450                
9451                columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements });
9452                columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements });
9453
9454                pageElements.push({
9455                  id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2,
9456                  columns,
9457                  backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9458                  backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9459                });
9460            }
9461
9462            newItem = {
9463                id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null,
9464                siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements,
9465                classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page'
9466            };
9467                } else if (classicItem.isFolder === true) {
9468            // --- Handle folders from import_guid.json format ---
9469            console.log(`Creating folder: "${title}"`);
9470            newItem = {
9471                id: newItemId, 
9472                title, 
9473                isFolder: true, 
9474                isCollapsed: classicItem.isCollapsed !== false, 
9475                visible: classicItem.visible !== false, 
9476                parentId: null,
9477                siteId: currentSiteId, 
9478                slug: finalSlug,
9479                importSource: 'folder'
9480            };
9481        } else {
9482            // --- This is a Gallery ---
9483            const galleryPageId = generatePageId();
9484            
9485            // --- ENHANCED: Try to fetch and include actual image data with captions ---
9486            let enhancedGalleryOptions = {
9487                manualCollectionName: `GUID=${itemGuid}`, 
9488                categoryId: classicItem.categoryID, 
9489                importedFromClassic: true,
9490                siteId: currentSiteId, 
9491                pageId: galleryPageId
9492            };
9493            
9494            // --- ENHANCED: Preserve gallery display settings from Classic JSON ---
9495            if (classicItem.galleryOptions) {
9496                console.log(`Preserving gallery settings for: ${title}`);
9497                
9498                // Preserve layout and display settings
9499                const preservedSettings = [
9500                    'startInSingles', 'layoutType', 'columns', 'spacing',
9501                    'showDescription', 'showFilename', 'displayAllInfo',
9502                    'navigationMode', 'showTextBlock', 'fixedHeroImage',
9503                    'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration',
9504                    'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor',
9505                    'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity',
9506                    'lightboxCloseColor', 'lightboxArrowColor', 'useTitles',
9507                    'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign',
9508                    'showDescriptionInOverlay', 'includeRolloverImageInOverlay',
9509                    'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles',
9510                    'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames',
9511                    'openMultipleLightboxes', 'batchSize', 'autoplaySingles',
9512                    'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart'
9513                ];
9514                
9515                preservedSettings.forEach(setting => {
9516                    if (classicItem.galleryOptions[setting] !== undefined) {
9517                        enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting];
9518                    }
9519                });
9520                
9521                // Preserve text content
9522                if (classicItem.galleryOptions.horizontalScrollerText) {
9523                    enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText;
9524                }
9525                
9526                // Preserve JSON collections if present
9527                if (classicItem.galleryOptions.jsonCollections) {
9528                    enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections;
9529                } else {
9530                    // Create jsonCollections structure with processed image metadata
9531                    const collectionKey = 'GUID=' + itemGuid;
9532                    const processedImages = classicItem.images || [];
9533                    
9534                    enhancedGalleryOptions.jsonCollections = {
9535                        [collectionKey]: {
9536                            isClassicCollection: true,
9537                            guid: collectionKey,
9538                            metadata: processedImages,
9539                            totalItems: processedImages.length
9540                        }
9541                    };
9542                    
9543                    console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images');
9544
9545                    // Build and try to upload NDJSON for this gallery.
9546                    // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON.
9547                    // Skip NDJSON generation for galleries with no images to avoid creating empty files
9548                    if (processedImages.length > 0) {
9549                      try {
9550                        console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)');
9551                        const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9552                        console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters');
9553                        
9554                        const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9555                      if (uploadResult && uploadResult.success) {
9556                        try {
9557                          // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent)
9558                          if (isImportFormat) {
9559                            const hydraCollectionKey = 'GUID=' + currentSiteId;
9560                            enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9561                            enhancedGalleryOptions.jsonCollections = {
9562                              [hydraCollectionKey]: {
9563                                isClassicCollection: false,
9564                                guid: hydraCollectionKey,
9565                                metadata: {},
9566                                totalItems: processedImages.length
9567                              }
9568                            };
9569                            enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9570                            enhancedGalleryOptions.initialPageUuid = galleryPageId;
9571                            enhancedGalleryOptions.isHydra = true;
9572                            enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9573                            console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)');
9574                          } else {
9575                            enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {};
9576                            enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length;
9577                          }
9578                          
9579                          // Store dual upload metadata if Irys was successful
9580                          if (uploadResult.irysResult && uploadResult.irysResult.txId) {
9581                            enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9582                            enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing
9583                            enhancedGalleryOptions.isPerma = true;
9584                            enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo;
9585                            enhancedGalleryOptions.fileSize = uploadResult.fileSize;
9586                            enhancedGalleryOptions.siteId = currentSiteId;
9587                            enhancedGalleryOptions.pageId = galleryPageId;
9588                            console.log('✅ Dual upload complete - Irys txId:', uploadResult.irysResult.txId);
9589                            console.log('   Uploaded to:', uploadResult.uploadedTo.join(' + '));
9590                          } else {
9591                            console.log('✅ Single upload (Fly.io only) - No Irys data');
9592                          }
9593                        } catch (applyErr) {
9594                          console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr);
9595                        }
9596                      } else {
9597                        console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array');
9598                      }
9599                      } catch (e) {
9600                        console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e);
9601                      }
9602                    } else {
9603                      console.log('Skipping NDJSON generation for "' + title + '" - no images to process');
9604                    }
9605                }
9606                
9607                // Preserve additional metadata
9608                if (classicItem.galleryOptions.siteAlias) {
9609                    enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias;
9610                }
9611                if (classicItem.galleryOptions.initialPageUuid) {
9612                    enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid;
9613                }
9614                if (classicItem.galleryOptions.initialPageAlias) {
9615                    enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias;
9616                }
9617                if (classicItem.galleryOptions.timestamp) {
9618                    enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp;
9619                }
9620            } else if (isImportFormat && (classicItem.images || []).length > 0) {
9621                // import_ format without galleryOptions: still create NDJSON for new Hydra galleries
9622                const processedImages = classicItem.images || [];
9623                const collectionKey = 'GUID=' + itemGuid;
9624                enhancedGalleryOptions.jsonCollections = {
9625                  [collectionKey]: {
9626                    isClassicCollection: true,
9627                    guid: collectionKey,
9628                    metadata: processedImages,
9629                    totalItems: processedImages.length
9630                  }
9631                };
9632                console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)');
9633                try {
9634                  const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9635                  const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9636                  if (uploadResult && uploadResult.success) {
9637                    const hydraCollectionKey = 'GUID=' + currentSiteId;
9638                    enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9639                    enhancedGalleryOptions.jsonCollections = {
9640                      [hydraCollectionKey]: {
9641                        isClassicCollection: false,
9642                        guid: hydraCollectionKey,
9643                        metadata: {},
9644                        totalItems: processedImages.length
9645                      }
9646                    };
9647                    enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9648                    enhancedGalleryOptions.initialPageUuid = galleryPageId;
9649                    enhancedGalleryOptions.isHydra = true;
9650                    enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9651                    if (uploadResult.irysResult?.txId) {
9652                      enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9653                      enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId;
9654                      enhancedGalleryOptions.isPerma = true;
9655                      enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly'];
9656                    }
9657                    console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON');
9658                  }
9659                } catch (e) {
9660                  console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e);
9661                }
9662            }
9663            
9664            // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions
9665            // Image data is only included when importing from Classic GUID directly
9666            console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`);
9667            
9668            newItem = {
9669                id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null,
9670                siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid,
9671                importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug,
9672                galleryOptions: enhancedGalleryOptions
9673            };
9674        }
9675
9676        tempNewItems.push(newItem);
9677    }
9678
9679    // --- Pass 2: Link children to parents using the ID map ---
9680    tempNewItems.forEach(newItem => {
9681        const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id);
9682        const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId));
9683
9684        if (originalItem && originalItem.parentId) {
9685            const newParentId = originalIdToNewIdMap[originalItem.parentId];
9686            if (newParentId) {
9687                newItem.parentId = newParentId;
9688                const parentItem = tempNewItems.find(p => p.id === newParentId);
9689                if (parentItem) {
9690                    parentItem.isSubmenu = true;
9691                    parentItem.isCollapsed = true;
9692                }
9693            } else {
9694                newItem.parentId = submenuParentId;
9695            }
9696        } else {
9697            newItem.parentId = submenuParentId;
9698        }
9699    });
9700
9701    if (replaceMenu) {
9702      console.log("Replacing existing menu structure.");
9703      // If replacing, overwrite the entire galleries array.
9704      galleries = tempNewItems;
9705    } else {
9706      console.log("Appending to existing menu structure.");
9707      // If not replacing, push the new items to the existing array.
9708      galleries.push(...tempNewItems);
9709    }
9710
9711
9712    
9713    if (progressBar) progressBar.style.width = '90%';
9714    if (statusMessage) statusMessage.textContent = 'Saving changes...';
9715
9716    // Pass import flag to skip localStorage for large import operations
9717    await saveGalleries({ isImport: true });
9718
9719    if (progressBar) progressBar.style.width = '100%';
9720    if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`;
9721    if (statusEl) statusEl.classList.remove('importing');
9722
9723    setTimeout(() => {
9724        renderGalleries();
9725        toggleImportClassicForm();
9726        if (progressBar) progressBar.style.width = '0';
9727        if (statusEl) statusEl.style.display = 'none';
9728        if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) {
9729            window.location.reload();
9730        }
9731    }, 1000);
9732
9733  } catch (error) {
9734    console.error('Error importing classic galleries:', error);
9735    if (statusMessage) statusMessage.textContent = `Error: ${error.message}`;
9736    if (progressBar) {
9737      progressBar.style.width = '100%';
9738      progressBar.style.backgroundColor = '#dc3545';
9739    }
9740    if (statusEl) statusEl.classList.remove('importing');
9741  }
9742}
9743
9744
9745
9746/**
9747 * Improved toggleImportClassicForm - Closes other forms first
9748 */
9749function toggleImportClassicForm() {
9750  // First check if user is logged in and in edit mode
9751  if (!isAuthenticated || !isAdmin || !window.isInEditMode()) {
9752    alert('You must be logged in as an admin and in edit mode to import galleries.');
9753    return;
9754  }
9755
9756  const formId = 'importClassicForm';
9757  const form = document.getElementById(formId);
9758  
9759  if (!form) {
9760    console.error('Import form element not found');
9761    return;
9762  }
9763  
9764  // If this form is already open, just close everything
9765  if (window.currentOpenForm === formId) {
9766    closeAllForms();
9767    return;
9768  }
9769  
9770  // Otherwise close all forms and open this one
9771  closeAllForms();
9772  
9773  // Show the form
9774  form.style.display = 'block';
9775  
9776  // Use setTimeout to allow the display change to take effect before adding the class
9777  setTimeout(() => {
9778    form.classList.add('visible');
9779  }, 10);
9780
9781  // Populate parent options
9782  const parentSelect = document.getElementById('importParent');
9783  if (parentSelect) {
9784    parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>';
9785    
9786    // Add all galleries that could be parents (including submenus)
9787    galleries.forEach(gallery => {
9788      // Only include items that are not already marked as imported
9789      if (!gallery.importSource) {
9790        parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
9791      }
9792    });
9793  }
9794
9795  // Set default values
9796  const submenuTitle = document.getElementById('submenuTitle');
9797  if (submenuTitle && !submenuTitle.value) {
9798    submenuTitle.value = 'Classic Galleries';
9799  }
9800
9801  // Reset any previous import status
9802  const statusEl = document.getElementById('importStatus');
9803  if (statusEl) {
9804    statusEl.style.display = 'none';
9805    const progressBar = statusEl.querySelector('.progress-bar');
9806    if (progressBar) {
9807      progressBar.style.width = '0';
9808      progressBar.style.backgroundColor = '#4682B4'; // Reset to blue
9809    }
9810  }
9811  
9812  // Set this as the current open form
9813  window.currentOpenForm = formId;
9814}
9815
9816// Add event listener for the radio buttons to adjust height when page is selected
9817document.addEventListener('DOMContentLoaded', function() {
9818  const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]');
9819  galleryTypeRadios.forEach(radio => {
9820    radio.addEventListener('change', function() {
9821      const addForm = document.getElementById('addForm');
9822      if (addForm && addForm.classList.contains('visible')) {
9823        const isPageSelected = this.value === 'page';
9824        
9825        // Give more height for page type
9826        if (isPageSelected) {
9827          addForm.style.maxHeight = '600px';
9828        } else {
9829          // For other types, measure content height
9830          const contentHeight = addForm.scrollHeight + 30;
9831          addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px';
9832        }
9833      }
9834    });
9835  });
9836});
9837
9838
9839/**
9840 * Improved toggleStyleEditor - Closes other forms first
9841 */
9842function toggleStyleEditor(display = false, event) {
9843  const formId = 'menuStyleEditor';
9844  const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen;
9845
9846  if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) {
9847    if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') {
9848      window.MenuStyleCustomizer._toggleOverlayMinimized(false);
9849    }
9850    return;
9851  }
9852
9853  if (display === false || window.currentOpenForm === formId || overlayOpen) {
9854    closeAllForms();
9855    return;
9856  }
9857
9858  closeAllForms();
9859
9860  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') {
9861    window.MenuStyleCustomizer.toggleOverlay();
9862  }
9863}
9864
9865
9866/**
9867 * This function closes all forms when escape key is pressed
9868 */
9869document.addEventListener('keydown', function(e) {
9870  if (e.key === 'Escape' && window.currentOpenForm) {
9871    if (closeTopmostStyleCustomizerChildOverlay()) {
9872      return;
9873    }
9874    if (window.MenuStyleCustomizer &&
9875        window.MenuStyleCustomizer._overlayOpen &&
9876        window.MenuStyleCustomizer._overlayMinimized) {
9877      return;
9878    }
9879    closeAllForms();
9880  }
9881});
9882
9883/**
9884 * This function closes all forms when clicking outside any form
9885 */
9886document.addEventListener('click', function(e) {
9887  if (!window.currentOpenForm) return;
9888
9889  if (window.MenuStyleCustomizer &&
9890      window.MenuStyleCustomizer._overlayOpen &&
9891      window.MenuStyleCustomizer._overlayMinimized) {
9892    return;
9893  }
9894
9895  // Keep Style Customizer and its nested panels open during in-overlay interactions.
9896  if (e.target.closest(
9897    '.pe-style-customizer-overlay, .pe-custom-code-overlay, .pe-palette-explorer-overlay, .pe-font-explorer-overlay, .pe-style-history-overlay, .sc-style-history-overlay, .pe-unsaved-changes-overlay, #colorMixerPopup, #artPaletteBtn, #exploreFontsBtn, #customCodeBtn, #colorMixerDot'
9898  )) {
9899    return;
9900  }
9901
9902  if (e.target.closest('.image-uploader-overlay')) {
9903    return;
9904  }
9905
9906  const currentForm = document.getElementById(window.currentOpenForm);
9907  if (!currentForm) {
9908    if (e.target.closest(
9909      '.pe-style-customizer-overlay, .pe-custom-code-overlay, .pe-palette-explorer-overlay, .pe-font-explorer-overlay, .pe-style-history-overlay, .sc-style-history-overlay, .pe-unsaved-changes-overlay, #colorMixerPopup'
9910    )) {
9911      return;
9912    }
9913    closeAllForms();
9914    return;
9915  }
9916
9917  const isInsideForm = currentForm.contains(e.target);
9918  const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null;
9919
9920  if (isInsideForm || isFormToggleButton) {
9921    return;
9922  }
9923
9924  closeAllForms();
9925});
9926
9927// Override the original functions with our improved versions
9928window.toggleAddForm = toggleAddForm;
9929window.toggleStyleEditor = toggleStyleEditor;
9930window.toggleMetadataEditor = toggleMetadataEditor;
9931window.toggleSidebarElementForm = toggleSidebarElementForm;
9932window.toggleImportClassicForm = toggleImportClassicForm;
9933window.closeAllForms = closeAllForms;
9934window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay;
9935
9936
9937/**
9938 * Opens the edit form for a gallery item.
9939 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu)
9940 * @param {number} id - The ID of the gallery item.
9941 * @param {Event} event - The click event.
9942 */
9943function editGallery(id, event) {
9944  event.stopPropagation(); // Prevent event bubbling
9945  const gallery = findGalleryById(galleries, id);
9946  const li = getGalleryTreeListItem(id, event);
9947
9948  if (gallery && li) {
9949    const showUrlField = gallery.isPage || gallery.isIntegrated;
9950    const showExternalUrlField = gallery.isExternal;
9951    const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal &&
9952      (isPageMenuItem(gallery) || gallery.isIntegrated === true);
9953    /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */
9954    const showSidebarSeoFields = !gallery.isPage;
9955    const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
9956    const currentExternalUrl = gallery.url || '';
9957
9958    // Clear existing content and rebuild form to ensure event listeners are fresh if needed
9959    li.innerHTML = ''; // Clear previous form if any
9960
9961    const formDiv = document.createElement('div');
9962    formDiv.className = 'edit-form';
9963    formDiv.innerHTML = `
9964        <div class="form-group">
9965           <label>Title:</label>
9966           <input type="text" value="${gallery.title || ''}" class="edit-title">
9967        </div>
9968
9969        ${showUrlField ? `
9970        <div class="form-group">
9971           <label>URL Slug:</label>
9972           <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery">
9973           <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small>
9974        </div>
9975        ` : ''}
9976
9977        ${showExternalUrlField ? `
9978        <div class="form-group">
9979           <label>External URL:</label>
9980           <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com">
9981           <small>Full URL for the external link (e.g., https://example.com/page).</small>
9982        </div>
9983        ` : ''}
9984
9985        ${showSidebarSeoFields ? `
9986        <div class="form-group">
9987           <label>Meta Title (Optional):</label>
9988           <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)">
9989           <small>Overrides site title for this item in search results/browser tabs.</small>
9990        </div>
9991
9992        <div class="form-group">
9993           <label>Meta Description (Optional):</label>
9994           <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea>
9995           <small>Overrides site description for this item in search results.</small>
9996        </div>
9997        ` : `
9998        <div class="form-group">
9999           <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;">
10000             Search title and description for this page are set in <strong>Page → metadata</strong> (the metadata block on the page), not here.
10001           </p>
10002        </div>
10003        `}
10004
10005        <div class="form-group">
10006          <label style="display: flex; align-items: center; gap: 8px;">
10007            <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10008            Hide menu when viewing this page
10009          </label>
10010          <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small>
10011        </div>
10012
10013        <div class="form-group">
10014          <label style="display: flex; align-items: center; gap: 8px;">
10015            <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10016            Show menu as overlay on this page
10017          </label>
10018          <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small>
10019        </div>
10020
10021        ${showMenuParentOption ? `
10022        <div class="form-group">
10023          <label style="display: flex; align-items: center; gap: 8px;">
10024            <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10025            Allow nested menu items
10026          </label>
10027          <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small>
10028        </div>
10029        ` : ''}
10030
10031        <div class="form-group">
10032          <label style="display: flex; align-items: center; gap: 8px;">
10033            <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10034            Password protect this page
10035          </label>
10036          <small>Visitors will need to enter a password to access this page.</small>
10037        </div>
10038
10039        <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};">
10040          <label>Password:</label>
10041          <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password">
10042          <small>Password visitors must enter to access this page.</small>
10043        </div>
10044
10045        <div class="form-group">
10046          <label style="display: flex; align-items: center; gap: 8px;">
10047            <input type="checkbox" class="edit-no-index" ${(() => {
10048              // Explicit setting takes priority;
10048 otherwise default to checked for hidden non-home pages
10049              if (gallery.noIndex === true) return 'checked';
10050              if (gallery.noIndex === false) return '';
10051              return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : '';
10052            })()} style="width: auto; margin-bottom: 0;">
10053            Hide from search engines (noindex)
10054          </label>
10055          <small>When checked, search engines will not index this page. Automatically enabled for hidden pages — uncheck to allow indexing even when hidden.</small>
10056        </div>
10057
10058        <div class="edit-form-actions"> 
10059          <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 
10060          <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 
10061        </div>
10062        <br>
10063    `;
10064    li.appendChild(formDiv); // Append the new form
10065
10066    // Add event listeners programmatically
10067    const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`);
10068    if (saveButton) {
10069        saveButton.addEventListener('click', (e) => saveEdit(id, e));
10070    }
10071
10072    const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`);
10073    if (cancelButton) {
10074        cancelButton.addEventListener('click', () => renderGalleries());
10075    }
10076
10077    // Add password protection toggle functionality
10078    const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected');
10079    const passwordField = formDiv.querySelector('.password-field');
10080    if (passwordProtectedCheckbox && passwordField) {
10081        passwordProtectedCheckbox.addEventListener('change', function() {
10082            passwordField.style.display = this.checked ? 'block' : 'none';
10083        });
10084    }
10085
10086    li.classList.add('edit-mode');
10087  } else {
10088    console.error("Could not find gallery or list item for ID:", id);
10089  }
10090}
10091
10092/**
10093 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use
10094 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first
10095 * and break nested folder toggles in edit mode while horizontal layout is on.
10096 * @param {number} id - Gallery ID
10097 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree)
10098 * @returns {HTMLElement|null}
10099 */
10100function getGalleryTreeListItem(id, event) {
10101  var numericId = typeof id === 'number' ? id : parseInt(String(id), 10);
10102  if (isNaN(numericId)) return null;
10103  var galleryTreeEl = document.getElementById('galleryTree');
10104  if (event && event.target && galleryTreeEl) {
10105    var clicked = event.target.closest('li[data-id]');
10106    if (clicked && galleryTreeEl.contains(clicked)) {
10107      var cid = parseInt(clicked.getAttribute('data-id') || '', 10);
10108      if (cid === numericId) return clicked;
10109    }
10110  }
10111  if (galleryTreeEl) {
10112    var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]');
10113    if (scoped) return scoped;
10114  }
10115  return document.querySelector('li[data-id="' + numericId + '"]');
10116}
10117
10118/**
10119 * Handles click on a folder item to show/hide its contents
10120 * @param {number} id - The gallery ID
10121 * @param {Event} event - The click event
10122 */
10123function toggleFolder(id, event) {
10124  // Ensure the event doesn't interfere with drag operations
10125  if (event) {
10126    event.stopPropagation();
10127  }
10128  
10129  
10130  const gallery = galleries.find(g => g.id === id);
10131  if (!gallery) {
10132    return;
10133  }
10134
10135  if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) {
10136    return;
10137  }
10138  
10139  
10140  // Toggle the collapsed state
10141  gallery.isCollapsed = !gallery.isCollapsed;
10142  
10143  // Find the list item in the DOM (#galleryTree first — not horizontal li.horizontal-submenu-item)
10144  const listItem = getGalleryTreeListItem(id, event);
10145  if (!listItem) {
10146    return;
10147  }
10148  
10149  
10150  // Toggle the expanded class FIRST - this is important for CSS to work correctly
10151  listItem.classList.toggle('expanded', !gallery.isCollapsed);
10152
10153  // Keep ARIA in sync for folder label + chevron (screen readers / keyboard)
10154  const folderExpanded = !gallery.isCollapsed;
10155  listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => {
10156    el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false');
10157  });
10158  
10159  // Force a reflow after toggling the class to ensure CSS recalculates
10160  void listItem.offsetHeight;
10161  
10162    // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal
10163    // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout
10164    // Use contains() instead of closest() because closest() only checks ancestors
10165    const galleryTree = document.getElementById('galleryTree');
10166    const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10167    const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10168    // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10168-container AND not in galleryTree
10169    const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar;
10170    
10171    let nestedList = null;
10172    
10173    // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures
10174    // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar
10175    if (isInGalleryTree) {
10176      nestedList = listItem.querySelector('ol.nested-sortable');
10177      
10178      // Also check direct children as fallback
10179      if (!nestedList) {
10180        const directChildren = Array.from(listItem.children);
10181        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10182      }
10183    } else if (isInSidebar) {
10184      // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar
10185      nestedList = listItem.querySelector('ol.nested-sortable');
10186      if (!nestedList) {
10187        const directChildren = Array.from(listItem.children);
10188        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10189      }
10190    }
10191    
10192    // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree)
10193    if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) {
10194      nestedList = listItem.querySelector('.horizontal-submenu');
10195      if (!nestedList) {
10196        nestedList = listItem.querySelector('.horizontal-dropdown');
10197      }
10198      if (!nestedList) {
10199        // For horizontal nested dropdowns, they're appended to document.body, not the list item
10200        // They're linked via data-flyout-id attribute
10201        const flyoutId = listItem.getAttribute('data-flyout-id');
10202        if (flyoutId) {
10203          // Look specifically for div.horizontal-nested-dropdown, not the list item itself
10204          const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]');
10205          
10206          // Find the actual dropdown div, not the list item
10207          const flyout = Array.from(allElementsWithId).find(el => 
10208            el.classList.contains('horizontal-nested-dropdown') && el !== listItem
10209          );
10210          
10211          
10212          if (flyout) {
10213            nestedList = flyout.querySelector('.horizontal-nested-list') || flyout;
10214          }
10215        }
10216      }
10217      if (!nestedList) {
10218        nestedList = listItem.querySelector('.horizontal-nested-dropdown');
10219      }
10220    }
10221    
10222    // Fallback: try all selectors if not found yet
10223    if (!nestedList) {
10224      nestedList = listItem.querySelector('ol.nested-sortable');
10225    }
10226    if (!nestedList) {
10227      // Debug: Log the actual DOM structure to understand what's there
10228      
10229      // Try to find any nested structure at all
10230      const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]');
10231      if (anyNested) {
10232        nestedList = anyNested; // Use it anyway
10233      }
10234      
10235      // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector
10236      // This can happen if the element was removed/re-added or if there's a timing issue
10237      const directChildren = Array.from(listItem.children);
10238      const olChild = directChildren.find(child => child.tagName === 'OL');
10239      if (olChild) {
10240        nestedList = olChild;
10241      }
10242    }
10243    
10244    if (!nestedList) {
10245      
10246      // If the nested list doesn't exist, we need to create it
10247      // This can happen if children aren't rendered until the folder is opened
10248      // First try flat array
10249      let gallery = galleries.find(g => g.id === id);
10250      
10251      // If gallery not found or has no children in flat array, build tree structure to find it
10252      if (!gallery || !gallery.children || gallery.children.length === 0) {
10253        
10254        // Build tree structure from flat array
10255        const buildTree = (items, parentId = null) => {
10256          return items
10257            .filter(item => item.parentId === parentId)
10258            .map(item => ({
10259              ...item,
10260              children: buildTree(items, item.id)
10261            }));
10262        };
10263        
10264        const galleryTree = buildTree(galleries);
10265        
10266        // Recursive function to find gallery in tree
10267        const findInTree = (items) => {
10268          for (const item of items) {
10269            if (item.id === id) {
10270              return item;
10271            }
10272            if (item.children && item.children.length > 0) {
10273              const found = findInTree(item.children);
10274              if (found) return found;
10275            }
10276          }
10277          return null;
10278        };
10279        
10280        const treeGallery = findInTree(galleryTree);
10281        if (treeGallery) {
10282          gallery = treeGallery;
10283        }
10284      }
10285      
10286        if (gallery && gallery.children && gallery.children.length > 0) {
10287          
10288          // Determine nesting level from list item
10289          const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10);
10290          const nextLevel = currentLevel + 1;
10291          
10292          // Create the nested list element
10293          nestedList = document.createElement('ol');
10294          nestedList.id = 'gallery-children-' + id;
10295          nestedList.className = 'nested-sortable';
10296          if (nextLevel > 0) {
10297            nestedList.classList.add('nested-level-' + nextLevel);
10298          }
10299          
10300          // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10301          gallery.children.forEach(child => {
10302            const childHtml = renderGalleryItem(child, nextLevel, 20);
10303            const tempDiv = document.createElement('div');
10304            tempDiv.innerHTML = childHtml;
10305            const childLi = tempDiv.querySelector('li');
10306            if (childLi) {
10307              nestedList.appendChild(childLi);
10308            }
10309          });
10310          
10311          // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context
10312          // If we're in #galleryTree (sidebar), skip all horizontal menu logic
10313          const galleryTree = document.getElementById('galleryTree');
10314          const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10315          const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10316          
10317          if (!isInGalleryTree && !isInSidebar) {
10318            const isInHorizontalMenu = listItem.closest('.horizontal-menu-container');
10319            if (isInHorizontalMenu) {
10320              const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown');
10321              if (horizontalDropdown && horizontalDropdown !== listItem) {
10322                horizontalDropdown.style.setProperty('display', 'block', 'important');
10323                horizontalDropdown.style.setProperty('visibility', 'visible', 'important');
10324                horizontalDropdown.style.setProperty('opacity', '1', 'important');
10325              }
10326            }
10327          }
10328          
10329          // CRITICAL: Ensure list item itself is visible and has correct display
10330          listItem.style.setProperty('display', 'list-item', 'important');
10331          listItem.style.setProperty('visibility', 'visible', 'important');
10332          
10333          // CRITICAL: Ensure ALL parent elements up to the sidebar are visible
10334          // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu!
10335          let parent = listItem.parentElement;
10336          let depth = 0;
10337          while (parent && depth < 10) {
10338            // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10339            if (parent.classList.contains('horizontal-menu-container')) {
10340              parent = parent.parentElement;
10341              depth++;
10342              continue;
10343            }
10344            
10345            const parentStyles = window.getComputedStyle(parent);
10346            if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10347              if (parentStyles.display === 'none') {
10348                parent.style.setProperty('display', 'block', 'important');
10349              }
10350              if (parentStyles.visibility === 'hidden') {
10351                parent.style.setProperty('visibility', 'visible', 'important');
10352              }
10353              if (parentStyles.opacity === '0') {
10354                parent.style.setProperty('opacity', '1', 'important');
10355              }
10356            }
10357            // Stop at sidebar or galleryTree
10358            if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') {
10359              break;
10360            }
10361            parent = parent.parentElement;
10362            depth++;
10363          }
10364          
10365          // Append directly to the list item (not to any wrapper)
10366          // Make sure we're not appending to a hidden parent
10367          listItem.appendChild(nestedList);
10368          
10369          // Immediately set the nested list to be visible since we're opening it
10370          nestedList.style.setProperty('display', 'block', 'important');
10371          nestedList.style.setProperty('visibility', 'visible', 'important');
10372          nestedList.style.setProperty('opacity', '1', 'important');
10373          nestedList.style.setProperty('height', 'auto', 'important');
10374          nestedList.style.setProperty('max-height', 'none', 'important');
10375          
10376          // Force immediate reflow after appending
10377          void nestedList.offsetHeight;
10378          void listItem.offsetHeight;
10379        }
10380    }
10381    
10382    if (nestedList) {
10383    if (!gallery.isCollapsed) {
10384      // Show the nested list - ensure parent has expanded class FIRST
10385      // CRITICAL: Add expanded class BEFORE any style operations
10386      listItem.classList.add('expanded');
10387      
10388      // Verify expanded class was added
10389      if (!listItem.classList.contains('expanded')) {
10390        listItem.className = listItem.className + ' expanded';
10391      }
10392      
10393      // CRITICAL: Ensure parent li is visible (it might be hidden by CSS)
10394      const parentVisibility = window.getComputedStyle(listItem).visibility;
10395      if (parentVisibility === 'hidden') {
10396        listItem.style.setProperty('visibility', 'visible', 'important');
10397      }
10398      
10399      // Force a reflow to ensure CSS recalculates after adding expanded class
10400      void listItem.offsetHeight;
10401      void nestedList.offsetHeight;
10402      
10403      // Show the nested list with !important to override CSS
10404      // Set these BEFORE removing any existing inline styles to avoid flicker
10405      nestedList.style.setProperty('display', 'block', 'important');
10406      nestedList.style.setProperty('visibility', 'visible', 'important');
10407      nestedList.style.setProperty('opacity', '1', 'important');
10408      nestedList.style.setProperty('height', 'auto', 'important');
10409      nestedList.style.setProperty('max-height', 'none', 'important');
10410      nestedList.style.setProperty('overflow', 'visible', 'important');
10411      
10412      // Also ensure the nested list is not clipped by parent overflow
10413      // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu!
10414      let checkParent = nestedList.parentElement;
10415      let checkDepth = 0;
10416      while (checkParent && checkDepth < 5) {
10417        // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu
10418        if (checkParent.classList.contains('horizontal-menu-container')) {
10419          checkParent = checkParent.parentElement;
10420          checkDepth++;
10421          continue;
10422        }
10423        
10424        const parentOverflow = window.getComputedStyle(checkParent).overflow;
10425        if (parentOverflow === 'hidden') {
10426          checkParent.style.setProperty('overflow', 'visible', 'important');
10427        }
10428        if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') {
10429          break;
10430        }
10431        checkParent = checkParent.parentElement;
10432        checkDepth++;
10433      }
10434      
10435      // Force another reflow after setting styles
10436      void nestedList.offsetHeight;
10437      
10438      const finalRect = nestedList.getBoundingClientRect();
10439      
10440      // If the nested list has zero dimensions, it's not actually visible
10441      if (finalRect.width === 0 || finalRect.height === 0) {
10442        // Force all parent elements to be visible
10443        // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu!
10444        let forceParent = nestedList.parentElement;
10445        while (forceParent && forceParent !== document.body) {
10446          // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10447          if (forceParent.classList.contains('horizontal-menu-container')) {
10448            forceParent = forceParent.parentElement;
10449            continue;
10450          }
10451          
10452          forceParent.style.setProperty('display', 'block', 'important');
10453          forceParent.style.setProperty('visibility', 'visible', 'important');
10454          forceParent.style.setProperty('opacity', '1', 'important');
10455          forceParent.style.setProperty('overflow', 'visible', 'important');
10456          if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') {
10457            break;
10458          }
10459          forceParent = forceParent.parentElement;
10460        }
10461        void nestedList.offsetHeight;
10462      }
10463      
10464      // Check for any parent elements that might be hiding the nested list and fix them
10465      let parent = nestedList.parentElement;
10466      let depth = 0;
10467      while (parent && depth < 5) {
10468        const parentStyles = window.getComputedStyle(parent);
10469        if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10470          
10471          // Fix hidden parent elements
10472          if (parentStyles.display === 'none') {
10473            parent.style.setProperty('display', 'block', 'important');
10474          }
10475          if (parentStyles.visibility === 'hidden') {
10476            parent.style.setProperty('visibility', 'visible', 'important');
10477          }
10478          if (parentStyles.opacity === '0') {
10479            parent.style.setProperty('opacity', '1', 'important');
10480          }
10481        }
10482        parent = parent.parentElement;
10483        depth++;
10484      }
10485      
10486    } else {
10487      // Hide the nested list
10488      nestedList.style.setProperty('display', 'none', 'important');
10489      nestedList.style.setProperty('visibility', 'hidden', 'important');
10490      nestedList.style.setProperty('opacity', '0', 'important');
10491    }
10492    
10493    // Force multiple reflows to ensure the browser applies all changes
10494    void listItem.offsetHeight;
10495    void nestedList.offsetHeight;
10496    void listItem.offsetWidth;
10497    void nestedList.offsetWidth;
10498    
10499    // Force a more aggressive repaint by temporarily toggling a class
10500    // This is especially important for sidebar layout where CSS rules might be interfer
10500ing
10501    listItem.classList.add('force-reflow');
10502    void listItem.offsetHeight;
10503    listItem.classList.remove('force-reflow');
10504    
10505    // Force reflow again after class manipulation
10506    void listItem.offsetHeight;
10507    void nestedList.offsetHeight;
10508    
10509    // Use requestAnimationFrame with multiple frames to ensure visual update
10510    // This ensures the browser has time to recalculate layout and repaint
10511    requestAnimationFrame(() => {
10512      void nestedList.offsetHeight;
10513      // Second frame to ensure all CSS has been recalculated
10514      requestAnimationFrame(() => {
10515        void nestedList.offsetHeight;
10516        // Verify the final state
10517        const finalDisplay = window.getComputedStyle(nestedList).display;
10518        const finalVisibility = window.getComputedStyle(nestedList).visibility;
10519        
10520        // If still not visible, force one more repaint cycle
10521        if (!gallery.isCollapsed && finalDisplay === 'none') {
10522          nestedList.style.setProperty('display', 'block', 'important');
10523          nestedList.style.setProperty('visibility', 'visible', 'important');
10524          void nestedList.offsetHeight;
10525          requestAnimationFrame(() => {
10526            void nestedList.offsetHeight;
10527          });
10528        }
10529      });
10530    });
10531    }
10532  
10533  // Find the submenu toggle icon and rotate it
10534  const toggleIcon = listItem.querySelector('.toggle-icon');
10535  if (toggleIcon) {
10536    toggleIcon.classList.toggle('rotated', !gallery.isCollapsed);
10537  }
10538  
10539  
10540  // Folder open/closed is a local UI preference — do not POST the full site config.
10541  try {
10542    const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}');
10543    folderStates[id] = !gallery.isCollapsed;
10544    localStorage.setItem('folder_states', JSON.stringify(folderStates));
10545  } catch (e) {
10546    console.error('Error saving folder state to localStorage:', e);
10547  }
10548}
10549
10550window.toggleFolder = toggleFolder;
10551
10552/**
10553 * Toggle password protection for a gallery/page
10554 * @param {number} id - The gallery ID
10555 * @param {Event} event - The click event
10556 */
10557function toggleGalleryPasswordProtection(id, event) {
10558  event.stopPropagation();
10559  
10560  const gallery = findGalleryById(galleries, id);
10561  if (!gallery) {
10562    console.error("Could not find gallery for password protection toggle, ID:", id);
10563    return;
10564  }
10565
10566  if (gallery.passwordProtected) {
10567    // Remove password protection
10568    if (confirm('Remove password protection from this page?')) {
10569      gallery.passwordProtected = false;
10570      delete gallery.password;
10571      saveGalleries();
10572      renderGalleries();
10573    }
10574  } else {
10575    // Add password protection
10576    const password = prompt('Enter a password for this page:');
10577    if (password && password.trim()) {
10578      gallery.passwordProtected = true;
10579      gallery.password = password.trim();
10580      saveGalleries();
10581      renderGalleries();
10582    }
10583  }
10584}
10585
10586window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection;
10587
10588function onPageMenuChromeToggle(checkbox) {
10589  if (!checkbox || !checkbox.checked) return;
10590  const root = checkbox.closest ? checkbox.closest('li') : null;
10591  if (!root) return;
10592  const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu';
10593  const sibling = root.querySelector(siblingClass);
10594  if (sibling && sibling !== checkbox) sibling.checked = false;
10595}
10596window.onPageMenuChromeToggle = onPageMenuChromeToggle;
10597
10598/**
10599 * Saves the edited gallery item details.
10600 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic)
10601 * @param {number} id - The ID of the gallery item.
10602 * @param {Event} event - The click event.
10603 */
10604function saveEdit(id, event) {
10605  event.stopPropagation();
10606  const li = getGalleryTreeListItem(id, event);
10607  if (!li) {
10608    console.error("Could not find list item for saveEdit, ID:", id);
10609    return;
10610  }
10611
10612  const titleInput = li.querySelector('.edit-title');
10613  const slugInput = li.querySelector('.edit-slug');
10614  const externalUrlInput = li.querySelector('.edit-external-url');
10615  const metaTitleInput = li.querySelector('.edit-meta-title');
10616  const metaDescriptionInput = li.querySelector('.edit-meta-description');
10617  const hideMenuInput = li.querySelector('.edit-hide-menu');
10618  const overlayMenuInput = li.querySelector('.edit-overlay-menu');
10619  const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent');
10620  const passwordProtectedInput = li.querySelector('.edit-password-protected');
10621  const passwordInput = li.querySelector('.edit-password');
10622  const noIndexInput = li.querySelector('.edit-no-index');
10623
10624  const title = titleInput ? titleInput.value.trim() : '';
10625  const newSlugRaw = slugInput ? slugInput.value.trim() : null;
10626  const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : '';
10627  const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : '';
10628  const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : '';
10629  const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10630  const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false;
10631  const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false;
10632  const password = passwordInput ? passwordInput.value.trim() : '';
10633  // noIndex is stored explicitly so it survives visibility changes without reverting
10634  const noIndex = noIndexInput ? noIndexInput.checked : undefined;
10635
10636  const gallery = findGalleryById(galleries, id);
10637  if (!gallery) {
10638    console.error("Could not find gallery object for saveEdit, ID:", id);
10639    renderGalleries();
10640    return;
10641  }
10642
10643  if (!title && !gallery.isSpacer) {
10644    alert('Please enter a title');
10645    return;
10646  }
10647
10648  // Validate password protection
10649  if (passwordProtected && !password) {
10650    alert('Please enter a password when password protection is enabled');
10651    return;
10652  }
10653
10654  // Only mutate the opt-in property when the user explicitly changes this checkbox.
10655  // This preserves the serialized shape and behavior of every existing item.
10656  if (actsAsMenuParentInput) {
10657    const wasNavigableMenuParent = gallery.actsAsMenuParent === true;
10658    const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true;
10659    if (wasNavigableMenuParent && !wantsNavigableMenuParent) {
10660      const hasNestedItems = galleries.some(item => item.parentId === gallery.id);
10661      if (hasNestedItems) {
10662        alert('Move all nested menu items out of this item before turning off “Allow nested menu items”.');
10663        return;
10664      }
10665      delete gallery.actsAsMenuParent;
10666    } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) {
10667      gallery.actsAsMenuParent = true;
10668    }
10669  }
10670
10671  gallery.title = title;
10672  // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page.
10673  if (!gallery.isPage) {
10674    gallery.metaTitle = metaTitle;
10675    gallery.metaDescription = metaDescription;
10676  }
10677  // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off.
10678  if (hideMenu) {
10679    gallery.hideMenuOnPage = true;
10680    delete gallery.overlayMenuOnPage;
10681  } else {
10682    gallery.hideMenuOnPage = hideMenu;
10683    if (overlayMenu) {
10684      gallery.overlayMenuOnPage = true;
10685    } else {
10686      delete gallery.overlayMenuOnPage;
10687    }
10688  }
10689  // Persist the explicit noIndex choice so it survives future visibility toggles
10690  if (noIndex !== undefined) gallery.noIndex = noIndex;
10691
10692  // Save password protection settings
10693  gallery.passwordProtected = passwordProtected;
10694  if (passwordProtected) {
10695    gallery.password = password;
10696  } else {
10697    delete gallery.password; // Remove password if protection is disabled
10698  }
10699
10700  if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) {
10701    const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10702    let sanitizedSlug = slugify(newSlugRaw);
10703
10704    if (!sanitizedSlug) {
10705      sanitizedSlug = slugify(gallery.title);
10706    }
10707
10708    if (sanitizedSlug !== originalSlug) {
10709      const otherGalleries = galleries.filter(g => g.id !== id);
10710      gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries);
10711      gallery.url = `/${gallery.slug}`;
10712    } else {
10713      if (gallery.url !== `/${gallery.slug}`) {
10714        gallery.url = `/${gallery.slug}`;
10715      }
10716    }
10717  }
10718
10719  // Update external URL if this is an external link
10720  if (gallery.isExternal && externalUrl) {
10721    gallery.url = externalUrl;
10722  }
10723
10724  saveGalleries();
10725  renderGalleries();
10726
10727  // If the currently active gallery was just edited, re-apply menu visibility
10728    if (gallery.id === activeGalleryId) {
10729    const bodyEl = document.body;
10730    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
10731        bodyEl.classList.add('menu-hidden-on-page');
10732    } else {
10733        bodyEl.classList.remove('menu-hidden-on-page');
10734    }
10735    applyMenuOverlayClassForGallery(gallery);
10736  }
10737}
10738// Helper function to check if a gallery is an ancestor of another
10739function hasAncestor(galleryId, ancestorId) {
10740if (galleryId === ancestorId) return true;
10741
10742const gallery = galleries.find(g => g.id === galleryId);
10743if (!gallery || !gallery.parentId) return false;
10744
10745if (gallery.parentId === ancestorId) return true;
10746
10747return hasAncestor(gallery.parentId, ancestorId);
10748}
10749
10750function deleteGallery(id, event) {
10751  event.stopPropagation();
10752  const gallery = galleries.find(g => g.id === id);
10753  const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item';
10754  if (!confirm('Are you sure you want to delete "' + title + '"?')) {
10755    return;
10756  }
10757  const overlay = showDeletionOverlay();
10758  overlay.updateMessage('Saving...');
10759  galleries = galleries.filter(g => g.id !== id);
10760  if (typeof window !== 'undefined') {
10761    window.galleries = galleries;
10762  }
10763  if (activeGalleryId === id) {
10764    activeGalleryId = null;
10765    const frame = document.getElementById('galleryFrame');
10766    if (frame) frame.src = '';
10767  }
10768  saveGalleries()
10769    .then(function(success) {
10770      renderGalleries();
10771      overlay.hide();
10772      if (success === false) {
10773        alert('Failed to delete. Please try again.');
10774      }
10775    })
10776    .catch(function(err) {
10777      overlay.hide();
10778      console.error('Error deleting item:', err);
10779      alert('Failed to delete. Please try again.');
10780    });
10781}
10782
10783function debugNestedStructure() {
10784console.group('Current Gallery Structure');
10785
10786function printGallery(gallery, level = 0) {
10787const indent = '  '.repeat(level);
10788console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`);
10789
10790if (gallery.children && gallery.children.length > 0) {
10791  gallery.children.forEach(child => printGallery(child, level + 1));
10792}
10793}
10794  // Create a tree structure for debugging
10795const galleryTree = createGalleryTree(galleries);
10796galleryTree.forEach(gallery => printGallery(gallery));
10797
10798console.groupEnd();
10799}
10800
10801function toggleHome(id, event) {
10802  event.stopPropagation();
10803  
10804  console.log('Setting home page to gallery:', id);
10805  
10806  // Find gallery
10807  const gallery = galleries.find(g => g.id === id);
10808  if (!gallery) return;
10809  
10810  // Check if this gallery is already set as home
10811  const alreadyHome = gallery.isHomePage === true;
10812  
10813  // Set all galleries to not be the home page
10814  galleries.forEach(g => {
10815    g.isHomePage = false;
10816  });
10817  
10818  // Toggle this gallery's home status - if it was already home, we're unsetting it
10819  gallery.isHomePage = !alreadyHome;
10820  
10821  // Save changes
10822  saveGalleries();
10823  
10824  // Update UI
10825  renderGalleries();
10826  
10827  // If this gallery is now the home page, display a message
10828  if (gallery.isHomePage) {
10829    const title = gallery.title || 'This page';
10830    showSuccessMessage(`${title} is now set as the Home page`);
10831  }
10832}
10833
10834// Function to toggle gallery visibility
10835function toggleVisibility(id, event) {
10836  event.stopPropagation();
10837  
10838  const gallery = galleries.find(g => g.id === id);
10839  if (gallery) {
10840    // Toggle the visibility
10841    gallery.visible = gallery.visible === false ? true : false;
10842    
10843    // Save the change to server
10844    saveGalleries();
10845    
10846    // Update UI - important to toggle the class on the button itself
10847    const toggle = event.currentTarget;
10848    if (toggle) {
10849      if (gallery.visible === false) {
10850        toggle.classList.add('hidden');
10851        
10852        // Update the SVG icon
10853        toggle.querySelector('svg').innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
10854      } else {
10855        toggle.classList.remove('hidden');
10856        
10857        // Update the SVG icon
10858        toggle.querySelector('svg').innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
10859      }
10860    }
10861    
10862    // Update parent row class (#galleryTree row, not horizontal submenu li)
10863    const listItem = getGalleryTreeListItem(id, event);
10864    if (listItem) {
10865      if (gallery.visible === false) {
10866        listItem.classList.add('hidden-gallery');
10867      } else {
10868        listItem.classList.remove('hidden-gallery');
10869      }
10870    }
10871    
10872    // Only clear iframe if not in edit mode
10873    if (id === activeGalleryId && gallery.visible === false && !isEditing) {
10874      document.getElementById('galleryFrame').src = '';
10875    }
10876    
10877    // Re-render menus based on current layout
10878    const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 
10879                         document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar';
10880    
10881    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
10882      window.renderHorizontalMenu();
10883    }
10884    
10885    // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied
10886    // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode
10887    if (typeof window.renderGalleries === 'function') {
10888      window.renderGalleries();
10889    } else if (typeof renderGalleries === 'function') {
10890      renderGalleries();
10891    }
10892  }
10893}
10894
10895
10896
10897/**
10898* Simplified loadGallery function that follows the same content swap pattern
10899* @param {number} id - The gallery ID
10900* @param {Event} event - Optional event object
10901*/
10902// Enhanced loadGallery function with script cleanup
10903async function loadGallery(id, event) {
10904
10905// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation
10906if (event) {
10907  if (typeof event.preventDefault === 'function') event.preventDefault();
10908  event.stopPropagation();
10909}
10910
10911const galleryContainer = document.querySelector('.gallery-container');
10912if (galleryContainer) {
10913  // Apply immediate hiding styles
10914  galleryContainer.style.opacity = '0';
10915}
10916
10917console.log('Loading gallery with ID:', id);
10918
10919if (typeof dismissGalleryPasswordOverlay === 'function') {
10920  dismissGalleryPasswordOverlay();
10921}
10922
10923// Find gallery by ID
10924const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
10925
10926if (!gallery) {
10927  console.warn('No gallery found with ID:', id);
10928  if (galleryContainer) {
10929  galleryContainer.style.opacity = '1'; 
10930  } 
10931  return;
10932}
10933
10934// Check if this is a page, if so use loadPage instead
10935if (isPageMenuItem(gallery)) {
10936  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
10937  return loadPage(id, event);
10938}
10939
10940// Skip submenu items
10941if (gallery.isSubmenu) {
10942  console.log('Gallery is submenu, skipping URL update');
10943  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
10944  if (galleryContainer) {
10945  galleryContainer.style.opacity = '1';
10946  } // Show container
10947  return;
10948}
10949
10950console.log('Found gallery:', gallery.title);
10951
10952window._activeContentType = 'gallery';
10953beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
10954if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
10955  window.PageManager.clearCurrentPage();
10956}
10957
10958// CRITICAL: Set active gallery ID consistently in both global and window scope
10959console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
10960window.activeGalleryId = id;
10961activeGalleryId = id; // Ensure both variables are synchronized
10962
10963// Save reference to which gallery we're loading (for later verification)
10964window.currentLoadingGalleryId = id;
10965
10966// CRITICAL: Thorough cleanup with await to ensure completion
10967if (typeof window.removeGalleryScriptsWithPause === 'function') {
10968  await window.removeGalleryScriptsWithPause();
10969}
10970
10971
10972// Clear all existing content from gallery container
10973if (!galleryContainer) {
10974  console.error('Gallery container not found');
10975  return;
10976}
10977
10978// Force clear all content
10979galleryContainer.innerHTML = '';
10980
10981
10982  // CRITICAL: Double-check active gallery ID before updating UI
10983  if (window.activeGalleryId !== id || activeGalleryId !== id) {
10984    console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
10985    window.activeGalleryId = id;
10986    activeGalleryId = id;
10987  }
10988
10989// Apply menu visibility based on the gallery's setting and edit mode
10990const bodyEl = document.body;
10991if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
10992    bodyEl.classList.add('menu-hidden-on-page');
10993} else {
10994    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
10995}
10996applyMenuOverlayClassForGallery(gallery);
10997
10998// Update UI state using explicit window function references or local fallbacks
10999if (typeof window.updateActiveStates === 'function') {
11000  window.updateActiveStates();
11001} else if (typeof updateActiveStates === 'function') {
11002  updateActiveStates();
11003}
11004
11005if (typeof window.updateMobileTitle === 'function') {
11006  window.updateMobileTitle();
11007} else if (typeof updateMobileTitle === 'function') {
11008  updateMobileTitle();
11009}
11010
11011if (typeof window.closeMobileMenu === 'function') {
11012  window.closeMobileMenu();
11013} else if (typeof closeMobileMenu === 'function') {
11014  closeMobileMenu();
11015}
11016
11017// Update URL with gallery slug
11018if (typeof window.updateURLWithGallerySlug === 'function') {
11019  window.updateURLWithGallerySlug(gallery);
11020} else if (typeof updateURLWithGallerySlug === 'function') {
11021  updateURLWithGallerySlug(gallery);
11022}
11023
11024  // Create gallery container and load content
11025const neonGalleryContainer = document.createElement('div');
11026neonGalleryContainer.id = 'neon-gallery-container';
11027neonGalleryContainer.className = 'gallery-direct-content';
11028neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
11029neonGalleryContainer.style.width = '100%';
11030neonGalleryContainer.style.height = '100%';
11031galleryContainer.appendChild(neonGalleryContainer);
11032
11033// Add a cache-busting parameter to ensure script is freshly loaded
11034const timestamp = Date.now();
11035
11036// Set up gallery parameters
11037window.Parameters = {
11038  SiteAlias: window.location.hostname,
11039  InitialPageUuid: gallery.pageId,
11040  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
11041  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
11042  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11043  isHydra: true,
11044  galleryInstanceId: timestamp,
11045  // Added loadedGalleryId as per your original function
11046  loadedGalleryId: gallery.id
11047};
11048
11049// Debug: Check what galleryOptions contains
11050console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
11051
11052// Set up gallery config
11053window.neonGalleryConfig = {
11054  useData: true,
11055  useCDN: true,
11056  version: 'live',
11057  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
11058  layoutType: gallery.galleryOptions?.layoutType || "grid",
11059  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
11060  // This ensures that only properties from the current gallery are included, not from previous galleries
11061  ...(gallery.galleryOptions || {}),
11062  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
11063  // These fields are required for the gallery to determine if it should use Load Network racing
11064  loadTxId: gallery.galleryOptions?.loadTxId || null,
11065  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
11066  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
11067  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
11068  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
11069  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
11070  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
11071  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
11072  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11073  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
11074  galleryId: gallery.id,
11075  galleryInstanceId: timestamp
11076};
11077
11078console.error('🔍 INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:', window.neonGalleryConfig.isPerma, 'permaURL:', window.neonGalleryConfig.permaURL, 'loadTxId:', window.neonGalleryConfig.loadTxId, 'loadPermaURL:', window.neonGalleryConfig.loadPermaURL, 'cdnNdjsonId:', window.neonGalleryConfig.cdnNdjsonId);
11079
11080// Create and add the gallery script with cache-busting
11081// Use Worker route (avoids CDN SSL issues)
11082const script = document.createElement('script');
11083const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
11084script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
11085script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
11086script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
11087console.log('Loading gallery-main script from Worker:', script.src);
11088script.onerror = function() {
11089  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
11090  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
11091  script.src = proxyUrl;
11092  script.onerror = function() {
11093    console.error('❌ Proxy failed, trying storage:', proxyUrl);
11094    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
11095  };
11096};
11097
11098// Added script.onload from your original function
11099script.onload = function() {
11100  if (window.currentLoadingGalleryId !== gallery.id) {
11101    console.warn('Gallery ID mismatch! Expected:', gallery.id,
11102                 'Current:', window.currentLoadingGalleryId);
11103  }
11104  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
11105};
11106
11107
11108if (galleryContainer) {
11109    setTimeout(() => {
11110      galleryContainer.style.opacity = '1';
11111    }, 50); // Small delay to ensure content is ready
11112  }
11113
11114console.log(`Loading fresh gallery script for: ${gallery.title}`);
11115document.body.appendChild(script);
11116}
11117
11118
11119
11120function removeGalleryScriptsWithPause() {
11121  return new Promise(resolve => {
11122    console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)');
11123    
11124    // 1. Capture and clear any gallery data in localStorage (remains the same)
11125    try {
11126      const localStorageKeys = Object.keys(localStorage);
11127      const galleryStorageKeys = localStorageKeys.filter(key => 
11128        key.includes('gallery') || 
11129        key.includes('neon') || 
11130        key.includes('lightbox')
11131      );
11132      galleryStorageKeys.forEach(key => {
11133        localStorage.removeItem(key);
11134      });
11135    } catch (e) {
11136      console.warn('Error clearing localStorage:', e);
11137    }
11138    
11139    // @ts-ignore
11140    window._galleryPageContext = null; // (remains the same)
11141    
11142    // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload;
11143    // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins.
11144    const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11145    galleryStyles.forEach(style => {
11146      const href = (style && style.href) ? String(style.href) : '';
11147      if (style && style.id === 'neon-gallery-css') return;
11148      if (href.indexOf('neon-gallery-hp-hydra') !== -1) return;
11149      if (style && style.parentNode) {
11150        style.parentNode.removeChild(style);
11151      }
11152    });
11153    
11154    // 4. Remove lightbox elements (more specific selectors now)
11155    // These are specific IDs and classes for lightbox components.
11156    const lightboxComponentSelectors = [
11157      '#neon-lightbox',      // Main lightbox container by ID
11158      '.neon-lightbox',     // Alternative class for main container
11159      '.lightbox-container',// General container class if used
11160      // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements
11161    ];
11162    const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', '));
11163    lightboxElements.forEach(lightboxEl => {
11164      if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) {
11165        // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className);
11166        lightboxEl.parentNode.removeChild(lightboxEl);
11167      }
11168    });
11169    
11170    // 5. Remove other gallery-specific DOM elements
11171    // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list
11172    // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body.
11173    const galleryElementSelectors = [
11174      '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay',
11175      '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu',
11176      '.neon-thumbnails', '.neon-image', '.neon-caption',
11177      '.neon-controls', '.neon-pagination',
11178      '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues.
11179      '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry'
11180      // Ensure none of these selectors inadvertently match document.body or critical layout containers
11181    ];
11182    const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', '));
11183    
11184    if (galleryElements.length > 0) {
11185      galleryElements.forEach(el => {
11186        // CRITICAL FIX: Add checks to ensure we don't remove body or html
11187        if (el && el.parentNode && el !== document.body && el !== document.documentElement) {
11188          // console.log('Removing gallery-specific DOM element:', el.id || el.className);
11189          el.parentNode.removeChild(el);
11190        } else if (el === document.body || el === document.documentElement) {
11191          console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`);
11192        }
11193      });
11194    }
11195    
11196    // 6. Find and remove ALL gallery scripts (remains the same)
11197    const scripts = document.querySelectorAll(
11198      'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11199      'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11200    );
11201    scripts.forEach(script => {
11202      if (script && script.parentNode) {
11203        // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id'));
11204        script.parentNode.removeChild(script);
11205      }
11206    });
11207
11208    // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries)
11209    const optionsScripts = document.querySelectorAll(
11210      'script[src*="galleryOptions"], script[data-options-js]'
11211    );
11212    optionsScripts.forEach(script => {
11213      if (script && script.parentNode) {
11214        console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js'));
11215        script.parentNode.removeChild(script);
11216      }
11217    });
11218
11219    // NOTE: We intentionally do NOT remove gallery options CSS here
11220    // The CSS is needed across galleries and doesn't cause conflicts like scripts do
11221
11222    setTimeout(() => {
11223      const remainingScripts = document.querySelectorAll(
11224        'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11225        'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11226      );
11227      if (remainingScripts.length > 0) {
11228        // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`);
11229      }
11230    }, 50);
11231        
11232    // 7. Reset ALL known global state variables used by the gallery (remains the same)
11233    const resetGlobals = [
11234      'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
11235      'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState',
11236      'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings',
11237      'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance',
11238      'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache',
11239    ];
11240    
11241    resetGlobals.forEach(prop => {
11242      // @ts-ignore
11243      if (window[prop] !== undefined) {
11244        // @ts-ignore
11245        window[prop] = null;
11246      }
11247    });
11248    // Keep the bootstrap wipe flag. Clearing it let a re-injected main script
11249    // replace PageManager embed config with the default "mod" collection.
11250    if (typeof window.resetNeonGalleryInitGates === 'function') {
11251      window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause');
11252    }
11253
11254    // 7b. Reset gallery options panel state
11255    if (window._editorButtonVisibilityInterval) {
11256      clearInterval(window._editorButtonVisibilityInterval);
11257      window._editorButtonVisibilityInterval = null;
11258    }
11259    const editorBtn = document.getElementById('editorButton');
11260    if (editorBtn) {
11261      editorBtn.classList.add('gallery-options-force-hidden');
11262      editorBtn.style.setProperty('display', 'none', 'important');
11263      editorBtn.style.pointerEvents = '';
11264    }
11265    if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
11266      window.syncGalleryOptionsButtonVisibility();
11267    }
11268    // Reset optionsVisible flag if it exists on window (for cross-module access)
11269    // @ts-ignore
11270    if (window.optionsVisible !== undefined) {
11271      // @ts-ignore
11272      window.optionsVisible = false;
11273    }
11274    
11275    // Reset options-related globals
11276    const optionsGlobals = [
11277      'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 
11278      'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible'
11279    ];
11280    optionsGlobals.forEach(prop => {
11281      // @ts-ignore
11282      if (window[prop] !== undefined && typeof window[prop] !== 'function') {
11283        // Only reset non-function properties (like flags)
11284        // Functions should remain available
11285        // @ts-ignore
11286        if (prop === 'optionsVisible') {
11287          // @ts-ignore
11288          window[prop] = false;
11289        }
11290      }
11291    });
11292    
11293    // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever
11294    // after scripts it was waiting on were just removed from the DOM.
11295    if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
11296      window.EnhancedGallerySystem.resetInitializationState();
11297    }
11298
11299    // Reset PageManager page-embedded gallery tracking
11300    window.neonGalleryScriptLoaded = false;
11301    window.pageManagerGalleryInitBlocked = false;
11302    if (window.pageManagerGalleryContainers) {
11303      window.pageManagerGalleryContainers.clear();
11304    }
11305
11306    // Remove orphaned lightbox keyboard listeners (survive script tag removal)
11307    if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
11308      window.removeAllLightboxKeyboardListeners();
11309    } else if (window.currentLightboxKeyboardListener) {
11310      document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
11311      window.currentLightboxKeyboardListener = null;
11312      window.lightboxKeyboardListenerActive = false;
11313    }
11314
11315    // Short pause before resolving
11316    setTimeout(resolve, 150);
11317  });
11318}
11319
11320
11321// Fixed clearAddForm function with proper variable names
11322function clearAddForm() {
11323// Clear the title input
11324const titleInput = document.getElementById('galleryTitle');
11325if (titleInput) {
11326titleInput.value = '';
11327}
11328
11329// Clear the URL input
11330const urlInput = document.getElementById('galleryUrl');
11331if (urlInput) {
11332urlInput.value = '';
11333}
11334
11335// Reset the parent selection
11336const parentSelect = document.getElementById('galleryParent');
11337if (parentSelect) {
11338parentSelect.value = '';
11339}
11340
11341// Reset radio buttons to Gallery (integrated) as default
11342const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]');
11343if (galleryRadio) {
11344galleryRadio.checked = true;
11345}
11346
11347// Hide URL input container when default is Gallery
11348const urlInputContainer = document.getElementById('urlInputContainer');
11349if (urlInputContainer) {
11350urlInputContainer.style.display = 'none';
11351} else {
11352console.log('URL input container not found');
11353}
11354}
11355
11356/**
11357 * Ensures window.galleries is synchronized with the global galleries variable
11358 * Call this function after any modification to gallery settings
11359 * 
11360 * @returns {boolean} True if synchronization was successful
11361 */
11362/**
11363 * Enhanced synchronizeGalleries function that checks ALL galleries
11364 * not just imported ones, and ensures settings are preserved
11365 * @returns {boolean} Success indicator
11366 */
11367function synchronizeGalleries() {
11368  console.log('Starting enhanced gallery synchronization...');
11369  
11370  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
11371    // First check if window.galleries exists and is different from galleries
11372    const needsSync = !window.galleries || 
11373                     !Array.isArray(window.galleries) || 
11374                     window.galleries.length !== galleries.length;
11375    
11376    // ENHANCED: Check ALL galleries for options preservation, not just imported ones
11377    if (!needsSync && window.galleries.length > 0) {
11378      // Find all galleries with galleryOptions
11379      const galleriesWithOptions = galleries.filter(g => 
11380        g.galleryOptions && Object.keys(g.galleryOptions).length > 4
11381      );
11382      
11383      if (galleriesWithOptions.length > 0) {
11384        // Check ALL galleries with substantial options, not just a sample
11385        let syncNeeded = false;
11386        
11387        // Use for loop so we can break early if we find a mismatch
11388        for (let i = 0; i < galleriesWithOptions.length; i++) {
11389          const gallery = galleriesWithOptions[i];
11390          const windowGallery = window.galleries.find(g => g.id === gallery.id);
11391          
11392          if (windowGallery) {
11393            // Get options depth
11394            const galleryOptionsDepth = gallery.galleryOptions ? 
11395              Object.keys(gallery.galleryOptions).length : 0;
11396              
11397            const windowOptionsDepth = windowGallery.galleryOptions ? 
11398              Object.keys(windowGallery.galleryOptions).length : 0;
11399            
11400            // If options depth differs significantly, we need to sync
11401            if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) {
11402              console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`);
11403              console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`);
11404              
11405              syncNeeded = true;
11406              break; // No need to check more galleries
11407            }
11408            
11409            // Additional check: Look for specific important gallery settings that should be preserved
11410            // Only do this check for galleries with substantial settings
11411            if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) {
11412              // Critical keys that indicate gallery customization
11413              const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 
11414                                  'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor'];
11415              
11416              // Check if any critical keys are missing in window.galleries
11417              const missingKeys = criticalKeys.filter(key => 
11418                gallery.galleryOptions[key] !== undefined && 
11419                windowGallery.galleryOptions[key] === undefined
11420              );
11421              
11422              if (missingKeys.length > 0) {
11423                console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`);
11424                console.log(`Missing keys: ${missingKeys.join(', ')}`);
11425                
11426                syncNeeded = true;
11427                break; // No need to check more galleries
11428              }
11429            }
11430          }
11431        }
11432        
11433        // If we detected a need to sync, do it now
11434        if (syncNeeded) {
11435          // Ensure we preserve rich gallery settings and per-page custom HTML fields
11436          window.galleries = galleries.map(gallery => {
11437            const windowGallery = window.galleries.find(g => g.id === gallery.id);
11438            if (!windowGallery) return gallery;
11439
11440            let merged = { ...gallery };
11441
11442            if (windowGallery.galleryOptions && gallery.galleryOptions) {
11443              const globalOptionsDepth = Object.keys(gallery.galleryOptions).length;
11444              const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length;
11445              if (windowOptionsDepth > globalOptionsDepth) {
11446                merged = {
11447                  ...merged,
11448                  galleryOptions: {
11449                    ...windowGallery.galleryOptions,
11450                    ...gallery.galleryOptions
11451                  }
11452                };
11453              }
11454            }
11455
11456            if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11457              merged = {
11458                ...merged,
11459                customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11460                customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11461                customPageHtmlFilename: windowGallery.customPageHtmlFilename
11462              };
11463            }
11464
11465            return merged;
11466          });
11467          
11468          console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11469          return true;
11470        }
11471      }
11472    }
11473    
11474    // If we determined sync is needed (or as a precaution), update window.galleries
11475    if (needsSync || !window.galleries) {
11476      if (window.galleries && Array.isArray(window.galleries)) {
11477        galleries = galleries.map(function(gallery) {
11478          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11479          if (!windowGallery) return gallery;
11480          if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11481            return {
11482              ...gallery,
11483              customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11484              customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11485              customPageHtmlFilename: windowGallery.customPageHtmlFilename
11486            };
11487          }
11488          return gallery;
11489        });
11490      }
11491      window.galleries = [...galleries]; // Create a shallow copy to ensure different reference
11492      console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11493    } else {
11494      console.log('Gallery synchronization check: No sync needed');
11495    }
11496    
11497    // Verify that all galleries with rich settings have their full options preserved
11498    const richGalleries = galleries.filter(g => 
11499      g.galleryOptions && Object.keys(g.galleryOptions).length > 5
11500    );
11501    
11502    console.log(`Status: Found ${richGalleries.length} galleries with rich settings`);
11503    
11504    // Final verification - keep both arrays aligned without dropping page-level fields
11505    if (window.galleries !== galleries) {
11506      console.warn('References not identical after sync - merging page fields into global galleries');
11507      if (window.galleries && Array.isArray(window.galleries)) {
11508        galleries = galleries.map(function(gallery) {
11509          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11510          if (!windowGallery) return gallery;
11511          return {
11512            ...gallery,
11513            customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled,
11514            customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key,
11515            customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename,
11516            hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage,
11517            overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage
11518          };
11519        });
11520      }
11521      window.galleries = galleries;
11522      return true;
11523    }
11524    
11525    return true;
11526  } else {
11527    console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array');
11528    
11529    // If we have window.galleries but no global galleries, try to reverse-sync
11530    if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) {
11531      console.log('Attempting reverse sync: updating global galleries from window.galleries');
11532      try {
11533        // This is risky and should only happen in unusual circumstances
11534        galleries = window.galleries;
11535        return true;
11536      } catch (e) {
11537        console.error('Could not reverse-sync galleries:', e);
11538      }
11539    }
11540    
11541    return false;
11542  }
11543}
11544
11545
11546
11547/**
11548 * Loads NDJSON content from the original gallery
11549 * @param {string} siteId - The site ID
11550 * @param {string} pageId - The page ID of the original gallery
11551 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error
11552 */
11553async function loadGalleryNDJSON(siteId, pageId) {
11554  console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId);
11555  
11556  if (!siteId || !pageId) {
11557    console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId });
11558    return null;
11559  }
11560
11561  try {
11562    const ndjsonFilename = `${siteId}_${pageId}.ndjson`;
11563    const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`;
11564    
11565    console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url);
11566    console.error('[NDJSON Duplication] Filename:', ndjsonFilename);
11567    
11568    const response = await fetch(url);
11569    
11570    console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText);
11571    
11572    if (!response.ok) {
11573      if (response.status === 404) {
11574        console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet');
11575        console.error('[NDJSON Duplication] Attempted URL:', url);
11576        return null;
11577      }
11578      const errorText = await response.text().catch(() => 'Could not read error text');
11579      console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText);
11580      throw new Error(`Failed to load NDJSON: ${response.statusText}`);
11581    }
11582    
11583    const ndjsonContent = await response.text();
11584    console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters');
11585    console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200));
11586    return ndjsonContent;
11587  } catch (error) {
11588    console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error);
11589    console.error('[NDJSON Duplication] Error message:', error.message);
11590    console.error('[NDJSON Duplication] Error stack:', error.stack);
11591    return null;
11592  }
11593}
11594
11595/**
11596 * Duplicates and publishes NDJSON for a gallery
11597 * @param {Object} originalGallery - The original gallery object
11598 * @param {Object} newGallery - The new gallery object with new pageId
11599 * @returns {Promise<boolean>} - Success indicator
11600 */
11601async function duplicateGalleryNDJSON(originalGallery, newGallery) {
11602  console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON =====');
11603  console.error('[NDJSON Duplication] Original gallery:', {
11604    id: originalGallery.id,
11605    title: originalGallery.title,
11606    pageId: originalGallery.pageId,
11607    classicGuid: originalGallery.classicGuid,
11608    hasGalleryOptions: !!originalGallery.galleryOptions,
11609    manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11610    isClassicCollection: originalGallery.galleryOptions?.isClassicCollection
11611  });
11612  console.error('[NDJSON Duplication] New gallery:', {
11613    id: newGallery.id,
11614    title: newGallery.title,
11615    pageId: newGallery.pageId
11616  });
11617  
11618  try {
11619    // Check if this is an NDJSON-based gallery (not classic collection)
11620    // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files
11621    // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON)
11622    // Having a pageId means it uses NDJSON files, so we should duplicate them
11623    const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true;
11624    
11625    console.error('[NDJSON Duplication] Classic collection check:', {
11626      hasClassicGuid: !!originalGallery.classicGuid,
11627      manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11628      startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11628UID='),
11629      isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection,
11630      hasPageId: !!originalGallery.pageId,
11631      isClassicCollection: isClassicCollection,
11632      willSkip: isClassicCollection && !originalGallery.pageId
11633    });
11634    
11635    // Only skip if explicitly marked as classic collection AND has no pageId
11636    // If it has a pageId, it uses NDJSON files and we should duplicate them
11637    if (isClassicCollection && !originalGallery.pageId) {
11638      console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication');
11639      return true; // Not an error, just not applicable
11640    }
11641
11642    // Check if original gallery has a pageId
11643    if (!originalGallery.pageId) {
11644      console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication');
11645      console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery));
11646      return true; // Not an error, just not applicable
11647    }
11648
11649    // Get siteId
11650    const siteId = window.siteId || originalGallery.siteId;
11651    console.error('[NDJSON Duplication] SiteId resolution:', {
11652      windowSiteId: window.siteId,
11653      gallerySiteId: originalGallery.siteId,
11654      resolvedSiteId: siteId
11655    });
11656    
11657    if (!siteId) {
11658      console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId');
11659      console.error('[NDJSON Duplication] window.siteId:', window.siteId);
11660      console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId);
11661      return false;
11662    }
11663
11664    // Load original NDJSON
11665    console.error('[NDJSON Duplication] Loading original NDJSON for duplication...');
11666    console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId);
11667    const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId);
11668    
11669    if (!originalNdjson) {
11670      console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet');
11671      console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found');
11672      return false;
11673    }
11674
11675    // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher
11676    console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId);
11677    const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`;
11678    console.error('[NDJSON Duplication] New filename:', dataFileName);
11679    console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length);
11680    
11681    try {
11682      // Hydra sites require Bearer auth (same token resolution as Edition Publisher)
11683      const publishHeaders = { 'Content-Type': 'application/json' };
11684      let authToken = null;
11685      try {
11686        if (window.TokenManager) {
11687          if (typeof window.TokenManager.getHydraAuthToken === 'function') {
11688            try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
11689          }
11690          if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
11691            try {
11692              const tokenWithBearer = window.TokenManager.getBestToken();
11693              authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ')
11694                ? tokenWithBearer.substring(7)
11695                : tokenWithBearer;
11696            } catch (e) {}
11697          }
11698        }
11699        if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
11700        if (!authToken) authToken = window.authToken || window.didToken;
11701        if (authToken) {
11702          publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
11703            ? 'Bearer ' + authToken
11704            : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
11705          console.error('[NDJSON Duplication] Auth token attached for publish');
11706        } else {
11707          console.error('[NDJSON Duplication] WARNING: No auth token — publish will likely fail with 401');
11708        }
11709      } catch (authErr) {
11710        console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message);
11711      }
11712
11713      // Call the publish endpoint (same as edition publisher)
11714      console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish');
11715      const response = await fetch('https://hydra-press-v2.fly.dev/publish', {
11716        method: 'POST',
11717        headers: publishHeaders,
11718        body: JSON.stringify({
11719          ndjsonContent: originalNdjson,
11720          dataFileName: dataFileName
11721        }),
11722      });
11723      
11724      console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText);
11725      console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries()));
11726      
11727      if (!response.ok) {
11728        const errorText = await response.text().catch(() => 'Unknown error');
11729        console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText);
11730        console.error('[NDJSON Duplication] Response status:', response.status);
11731        console.error('[NDJSON Duplication] Response statusText:', response.statusText);
11732        return false;
11733      }
11734      
11735      const result = await response.json();
11736      console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON');
11737      console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2));
11738      console.error('[NDJSON Duplication] Publish result URLs:', result.urls);
11739      
11740      // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions
11741      // with txId/permaURL here. If Irys upload is needed, it would need to be done separately.
11742      
11743      console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY =====');
11744      return true;
11745    } catch (error) {
11746      console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error);
11747      console.error('[NDJSON Duplication] Error message:', error.message);
11748      console.error('[NDJSON Duplication] Error stack:', error.stack);
11749      return false;
11750    }
11751  } catch (error) {
11752    console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error);
11753    console.error('[NDJSON Duplication] Error message:', error.message);
11754    console.error('[NDJSON Duplication] Error stack:', error.stack);
11755    return false;
11756  }
11757}
11758
11759/**
11760 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication).
11761 * @returns {Object} Object with hide() and updateMessage() to control the overlay
11762 */
11763function showDeletionOverlay() {
11764    const existingOverlay = document.getElementById('deletion-overlay');
11765    if (existingOverlay) existingOverlay.remove();
11766    if (!document.querySelector('#deletion-overlay-styles')) {
11767        const style = document.createElement('style');
11768        style.id = 'deletion-overlay-styles';
11769        style.textContent = `
11770            #deletion-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 99999999; display: flex; align-items: center; justify-content: center; cursor: not-allowed; pointer-events: all; }
11771            #deletion-overlay .deletion-message { background: rgba(255,255,255,0.95); padding: 30px 50px; font-family: Arial,sans-serif; font-size: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; flex-direction: column; align-items: center; gap: 20px; }
11772            #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; }
11773        `;
11774        document.head.appendChild(style);
11775    }
11776    const overlayEl = document.createElement('div');
11777    overlayEl.id = 'deletion-overlay';
11778    const messageEl = document.createElement('div');
11779    messageEl.className = 'deletion-message';
11780    const spinnerEl = document.createElement('div');
11781    spinnerEl.className = 'deletion-spinner';
11782    spinnerEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"><defs><filter id="svgSpinnersDeletion"><feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/><feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/><feBlend in="SourceGraphic" in2="z"/></filter></defs><g filter="url(#svgSpinnersDeletion)"><circle cx="5" cy="12" r="4" fill="currentColor"><animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/></circle><circle cx="19" cy="12" r="4" fill="currentColor"><animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/></circle><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></g></svg>`;
11783    const textEl = document.createElement('div');
11784    textEl.textContent = 'Deleting...';
11785    textEl.style.textAlign = 'center';
11786    messageEl.appendChild(spinnerEl);
11787    messageEl.appendChild(textEl);
11788    overlayEl.appendChild(messageEl);
11789    document.body.appendChild(overlayEl);
11790    return {
11791        hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); },
11792        updateMessage: function(msg) { textEl.textContent = msg; }
11793    };
11794}
11795
11796/**
11797 * Creates and shows a loading overlay for gallery duplication
11798 * @returns {Object} Object with hide function to remove the overlay
11799 */
11800function showDuplicationOverlay() {
11801    // Remove any existing overlay first
11802    const existingOverlay = document.getElementById('duplication-overlay');
11803    if (existingOverlay) {
11804        existingOverlay.remove();
11805    }
11806    
11807    // Add styles if not already present
11808    if (!document.querySelector('#duplication-overlay-styles')) {
11809        const style = document.createElement('style');
11810        style.id = 'duplication-overlay-styles';
11811        style.textContent = `
11812            #duplication-overlay {
11813                position: fixed;
11814                top: 0;
11815                left: 0;
11816                right: 0;
11817                bottom: 0;
11818                background: rgba(0, 0, 0, 0.5);
11819                z-index: 99999999;
11820                display: flex;
11821                align-items: center;
11822                justify-content: center;
11823                cursor: not-allowed;
11824                pointer-events: all;
11825            }
11826            #duplication-overlay .duplication-message {
11827                background: rgba(255, 255, 255, 0.95);
11828                padding: 30px 50px;
11829                font-family: Arial, sans-serif;
11830                font-size: 16px;
11831                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
11832                display: flex;
11833                flex-direction: column;
11834                align-items: center;
11835                gap: 20px;
11836            }
11837            #duplication-overlay .duplication-spinner {
11838                width: 80px;
11839                height: 80px;
11840                color: #444444;
11841            }
11842        `;
11843        document.head.appendChild(style);
11844    }
11845    
11846    const overlay = document.createElement('div');
11847    overlay.id = 'duplication-overlay';
11848    
11849    const message = document.createElement('div');
11850    message.className = 'duplication-message';
11851    
11852    const spinner = document.createElement('div');
11853    spinner.className = 'duplication-spinner';
11854    spinner.innerHTML = `
11855        <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
11856          <defs>
11857            <filter id="svgSpinnersGooeyBalls20">
11858              <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
11859              <feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/>
11860              <feBlend in="SourceGraphic" in2="z"/>
11861            </filter>
11862          </defs>
11863          <g filter="url(#svgSpinnersGooeyBalls20)">
11864            <circle cx="5" cy="12" r="4" fill="currentColor">
11865              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
11866            </circle>
11867            <circle cx="19" cy="12" r="4" fill="currentColor">
11868              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
11869            </circle>
11870            <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
11871          </g>
11872        </svg>
11873    `;
11874    
11875    const text = document.createElement('div');
11876    text.textContent = 'Duplicating gallery...';
11877    text.style.textAlign = 'center';
11878    
11879    message.appendChild(spinner);
11880    message.appendChild(text);
11881    overlay.appendChild(message);
11882    document.body.appendChild(overlay);
11883    
11884    return {
11885        hide: function() {
11886            if (overlay && overlay.parentNode) {
11887                overlay.parentNode.removeChild(overlay);
11888            }
11889        },
11890        updateMessage: function(newMessage) {
11891            text.textContent = newMessage;
11892        }
11893    };
11894}
11895
11896/**
11897 * Duplicates a gallery or page item.
11898 * @param {number} id - The ID of the item to duplicate.
11899 * @param {Event} event - The click event.
11900 */
11901async function duplicateGalleryItem(id, event) {
11902    event.stopPropagation();
11903    console.log(`Duplicating item with ID: ${id}`);
11904
11905    // Show loading overlay
11906    const overlay = showDuplicationOverlay();
11907
11908    // Find the original item in the global galleries array
11909    const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find
11910
11911    if (!originalItem) {
11912        console.error(`Cannot duplicate: Item with ID ${id} not found.`);
11913        overlay.hide();
11914        alert('Error: Could not find the item to duplicate.');
11915        return;
11916    }
11917
11918    if (isPageMenuItem(originalItem)) {
11919        overlay.updateMessage('Duplicating page...');
11920    }
11921
11922    try {
11923        // --- Create a Deep Copy ---
11924        // Using JSON.parse/stringify is a common way for simple object structures
11925        // Be cautious if your objects have Dates, functions, Maps, Sets, etc.
11926        const newItem = JSON.parse(JSON.stringify(originalItem));
11927
11928        // --- Modify Copied Item ---
11929        newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID
11930        newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title
11931
11932        // Generate new pageId if it's a page or integrated gallery
11933        if (newItem.isPage || newItem.isIntegrated) {
11934            newItem.pageId = generatePageId(); // Assign a new unique pageId
11935        }
11936
11937        // Generate new slug and URL
11938        const newBaseSlug = slugify(newItem.title); // Use the new title
11939        newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness
11940        newItem.url = `/${newItem.slug}`; // Update URL based on new slug
11941
11942        // Reset children (don't duplicate children for simplicity)
11943        newItem.children = [];
11944
11945        // Deep clone galleryOptions and pageElements, update pageId if needed
11946        if (newItem.galleryOptions) {
11947            newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions));
11948            // Update pageId within options if it exists and matches the old one
11949            if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) {
11950                newItem.galleryOptions.pageId = newItem.pageId;
11951            }
11952            // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original
11953            ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl',
11954             'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp',
11955             'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma']
11956              .forEach(function(field) { delete newItem.galleryOptions[field]; });
11957        }
11958        if (newItem.pageElements) {
11959            newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements));
11960             // Give new IDs to duplicated page elements
11961             newItem.pageElements.forEach(el => {
11962                 el.id = Date.now() + Math.floor(Math.random() * 10000);
11963             });
11964        }
11965
11966        // Reset home page status
11967        newItem.isHomePage = false;
11968
11969        // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) ---
11970        const isDuplicatingPage = isPageMenuItem(originalItem);
11971        console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', {
11972            isDuplicatingPage,
11973            newItemPageId: newItem.pageId,
11974            originalItemPageId: originalItem.pageId
11975        });
11976
11977        if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) {
11978            overlay.updateMessage('Duplicating gallery files...');
11979            console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...');
11980            const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem);
11981            if (!ndjsonDuplicated) {
11982                console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication');
11983                console.error('[Gallery Duplication] User can republish the gallery later if needed');
11984                alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.');
11985                // Don't block the duplication if NDJSON fails - user can republish later
11986            } else {
11987                console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully');
11988            }
11989        } else {
11990            console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', {
11991                isDuplicatingPage,
11992                hasNewPageId: !!newItem.pageId,
11993                hasOriginalPageId: !!originalItem.pageId,
11994                reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' :
11995                    !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId'
11996            });
11997        }
11998
11999        overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...');
12000
12001        // --- Insert into Galleries Array ---
12002        // Find the index of the original item
12003        const originalIndex = galleries.findIndex(item => item.id === id);
12004
12005        if (originalIndex > -1) {
12006            // Insert the new item right after the original
12007            galleries.splice(originalIndex + 1, 0, newItem);
12008            console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`);
12009
12010             // Update positions for all items after the insertion point
12011             for (let i = originalIndex + 1; i < galleries.length; i++) {
12012                 galleries[i].position = i; // Re-assign position based on new index
12013             }
12014
12015        } else {
12016            // Fallback: Add to the end if original couldn't be found (shouldn't happen)
12017            console.warn(`Original item ${id} not found in array, adding duplicate to the end.`);
12018            newItem.position = galleries.length;
12019            galleries.push(newItem);
12020        }
12021
12022        console.log('Rendering galleries immediately after duplication...');
12023        renderGalleries();
12024        // Update active states if necessary (though unlikely needed here)
12025        updateActiveStates();
12026
12027        // --- Save and Refresh ---
12028        console.log('Saving duplicated item...');
12029        saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh
12030            .then(success => {
12031                overlay.hide(); // Hide overlay before page refresh
12032                if (success) {
12033                    console.log('Duplicate saved successfully. Page will refresh.');
12034                    // No need to call renderGalleries() here because saveGalleries will trigger a reload.
12035                } else {
12036                    console.error('Failed to save the duplicated item.');
12037                    alert('Error: Could not save the duplicated item.');
12038                    // Optionally try to revert the galleries array change here if save fails
12039                }
12040            })
12041            .catch(error => {
12042                 overlay.hide(); // Hide overlay on error
12043                 console.error('Error saving duplicated item:', error);
12044                 alert('Error saving duplicated item.');
12045            });
12046
12047    } catch (error) {
12048        overlay.hide(); // Hide overlay on error
12049        console.error('Error duplicating item:', error);
12050        alert('An error occurred while duplicating the item.');
12051    }
12052}
12053
12054/**
12055 * saveGalleries function that always saves FULL data
12056 * Ensures gallery settings are preserved in both localStorage and server
12057 * 
12058 * @param {Object} customData - Optional custom data to save instead of galleries
12059 * @returns {Promise<boolean>} - Success indicator
12060 */
12061
12062function attachLiveSidebarElements(data) {
12063  if (!data || typeof data !== 'object') return data;
12064  var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function');
12065  if (hasGetter) {
12066    var live = window.SidebarManager.getElementsForSave();
12067    if (Array.isArray(live) && live.length > 0) {
12068      data.sidebarElements = live;
12069      return data;
12070    }
12071    delete data.sidebarElements;
12072    return data;
12073  }
12074  if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) {
12075    delete data.sidebarElements;
12076  }
12077  return data;
12078}
12079
12080async function saveGalleries(customData) {
12081  if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') {
12082    alert('You must be logged in as an admin to save changes');
12083    return false;
12084  }
12085
12086  if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) {
12087    console.warn('Save blocked: help sandbox is active');
12088    return false;
12089  }
12090
12091  function _hasHelpDemoInTree(items) {
12092    if (!Array.isArray(items)) return false;
12093    for (var i = 0; i < items.length; i++) {
12094      var g = items[i];
12095      if (!g) continue;
12096      if (g.isHelpDemo || Number(g.id) < 0) return true;
12097      if (g.children && _hasHelpDemoInTree(g.children)) return true;
12098    }
12099    return false;
12100  }
12101
12102  if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) {
12103    console.warn('Save blocked: active help demo gallery');
12104    return false;
12105  }
12106
12107  var savePayload = customData && customData.galleries ? customData.galleries : galleries;
12108  if (_hasHelpDemoInTree(savePayload)) {
12109    console.warn('Save blocked: payload contains help demo records');
12110    return false;
12111  }
12112
12113  // Prevent multiple simultaneous saves — queue the latest payload instead of dropping it
12114  if (window._saveInProgress) {
12115    console.log('Save already in progress, queueing latest payload');
12116    if (customData) {
12117      window._pendingSaveGalleriesData = attachLiveSidebarElements(customData);
12118    }
12119    return new Promise(function(resolve) {
12120      window._pendingSaveResolvers = window._pendingSaveResolvers || [];
12121      window._pendingSaveResolvers.push(resolve);
12122    });
12123  }
12124
12125  window._saveInProgress = true;
12126
12127  try {
12128    // CRITICAL: Synchronize galleries before saving to ensure we use the latest data
12129    if (typeof synchronizeGalleries === 'function') {
12130      synchronizeGalleries();
12131      console.log('Galleries synchronized before saving');
12132    }
12133
12134    // Try to get a valid token from multiple sources
12135    let token = null;
12136    let tokenSource = 'none';
12137    
12138    // First try the global didToken (traditional approach)
12139    if (didToken) {
12140      console.log('Using global didToken for save operation');
12141      token = didToken;
12142      tokenSource = 'didToken';
12143    } 
12144    // Next try TokenManager if available
12145    else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
12146      token = window.TokenManager.getToken();
12147      
12148      // Remove Bearer prefix if present (we'll add it later)
12149      if (token && token.startsWith('Bearer ')) {
12150        token = token.substring(7);
12151      }
12152      
12153      console.log('Using TokenManager token for save operation, length:', token ? token.length : 0);
12154      tokenSource = 'TokenManager';
12155    }
12156    // Fallback to localStorage
12157    else if (localStorage.getItem('hydra_auth_token')) {
12158      token = localStorage.getItem('hydra_auth_token');
12159      console.log('Using localStorage token for save operation, length:', token.length);
12160      tokenSource = 'localStorage';
12161    }
12162    // Last resort: try to get a fresh token
12163    else if (magic && magic.user) {
12164      try {
12165        console.log('Attempting to get fresh token from Magic');
12166        token = await magic.user.getIdToken();
12167        console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed');
12168        tokenSource = token ? 'magic' : 'none';
12169      } catch (e) {
12170        console.error('Error getting token from Magic:', e);
12171        tokenSource = 'magic_error';
12172      }
12173    }
12174    
12175    // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request
12176    const loggedInEmail = (window.userMetadata && window.userMetadata.email) ||
12177      localStorage.getItem('hydra_auth_email') ||
12178      '';
12179    console.log('[AUTH_SAVE] Token resolved:', {
12180      loggedInEmail: loggedInEmail || '(none)',
12181      tokenSource,
12182      tokenLength: token ? token.length : 0,
12183      tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)',
12184      hasToken: !!token,
12185      isAuthenticated,
12186      hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true',
12187      hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'),
12188      hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function')
12189    });
12190    
12191    if (!token) {
12192      console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.');
12193      throw new Error('Could not find a valid authentication token. Please log in again.');
12194    }
12195
12196    // IMPORTANT: Try to save to localStorage with isolated error handling
12197    // This ensures localStorage failures don't prevent server saves
12198    if (!customData) { // Only try localStorage for regular gallery saves
12199      try {
12200        // FIXED: Always save the FULL galleries data to localStorage
12201        localStorage.setItem('galleries', JSON.stringify(galleries));
12202        console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`);
12203      } catch (storageError) {
12204        // Now storage errors are isolated and non-fatal
12205        console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError);
12206      }
12207    } else if (customData && customData.isImport) {
12208      // Skip localStorage for import operations - data too large
12209      console.log('Skipping localStorage for import operation - data too large for local storage');
12210    }
12211
12212    // IMPORTANT: Ensure all pages have proper URLs and slugs before saving
12213    galleries.forEach(gallery => {
12214      if (gallery.isPage === true) {
12215        const pageSlug = slugify(gallery.title);
12216        
12217        // Make sure slug and URL are set correctly
12218        if (!gallery.slug || gallery.slug === '') {
12219          gallery.slug = pageSlug;
12220        }
12221        
12222        // Make sure URL points to slug
12223        if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) {
12224          gallery.url = `/${pageSlug}`;
12225        }
12226      }
12227    });
12228
12229    // Set up the data to save - ensure we're using the complete data
12230    let siteData;
12231    console.log('saveGalleries: customData received:', JSON.stringify(customData));
12232    console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata);
12233    console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title);
12234    console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics);
12235    if (customData) {
12236      // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar
12237      // If customData contains galleries (which it should for sidebar-initiated saves),
12238      // merge them with any existing settings to ensure nothing is lost
12239      if (customData.galleries && Array.isArray(customData.galleries)) {
12240        // First, ensure we preserve all current gallery settings from the existing galleries
12241        const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || [];
12242        
12243        // Create merged galleries array with preserved settings
12244        const mergedGalleries = customData.galleries.map(gallery => {
12245          const existingGallery = existingGalleries.find(g => g.id === gallery.id);
12246          
12247          // If this gallery exists in the current dataset, ensure all galleryOptions are preserved
12248          if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) {
12249            // Calculate settings depth
12250            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12251            const serverOptionsDepth = Object.keys(gallery.galleryOptions).length;
12252            
12253            // If server has fewer settings, merge them with existing settings
12254            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12255              console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12256              
12257              // Create a merged gallery with preserved settings
12258              return {
12259                ...gallery,
12260                galleryOptions: {
12261                  ...existingGallery.galleryOptions, // Start with all existing settings
12262                  ...gallery.galleryOptions    // Override with any new server settings
12263                }
12264              };
12265            }
12266          }
12267          
12268          // If no preservation needed, use server gallery as is
12269          return gallery;
12270        });
12271        
12272        // Use the merged galleries in customData
12273        siteData = {
12274          ...customData,
12275          galleries: mergedGalleries
12276        };
12277        
12278        console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`);
12279        console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData));
12280        console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata);
12281        console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title);
12282        console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics);
12283      } else {
12284        siteData = customData;
12285      }
12286    } else {
12287      const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries))
12288        ? window.galleries
12289        : galleries;
12290      siteData = {
12291        galleries: galleriesForSave
12292      };
12293      
12294      if (window.siteMetadata) {
12295        siteData.siteMetadata = window.siteMetadata;
12296      }
12297
12298      if (window.SidebarManager) {
12299        const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function'
12300          ? window.SidebarManager.getElementsForSave()
12301          : null;
12302        if (Array.isArray(liveSidebar) && liveSidebar.length > 0) {
12303          siteData.sidebarElements = liveSidebar;
12304        }
12305      }
12306    }
12307
12308    attachLiveSidebarElements(siteData);
12309
12310    // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12311    // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.)
12312    if (siteData.galleries && Array.isArray(siteData.galleries)) {
12313      siteData.galleries = siteData.galleries.map(g => {
12314        if (g && g.galleryOptions && typeof g.galleryOptions === 'object') {
12315          return { ...g, isPage: false };
12316        }
12317        return g;
12318      });
12319    }
12320
12321    // Get the save URL
12322    const saveUrl = typeof getApiUrl === 'function' ? 
12323      getApiUrl('/api/save-config') : 
12324      '/api/save-config';
12325    
12326    // Get user email from various sources
12327    const userEmail = (window.userMetadata && window.userMetadata.email) || 
12328                     localStorage.getItem('hydra_auth_email') || 
12329                     '';
12330    
12331    // Ensure token has proper format
12332    const formattedToken = token.startsWith('hydra:') ? token :
12333                          (token.includes('.') ? token : `hydra:${token}`);
12334    
12335    console.log('Making server save request...');
12336    console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData));
12337    console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata);
12338    console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title);
12339    console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics);
12340    
12341    // [AUTH_SAVE] Log for S+J download - request details (no full token)
12342    console.log('[AUTH_SAVE] Sending save request:', {
12343      saveUrl,
12344      authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`,
12345      userEmail: userEmail || '(empty)',
12346      tokenSource,
12347      formattedTokenLength: formattedToken.length
12348    });
12349    
12350    // Make the request
12351    let response = await fetch(saveUrl, {
12352      method: 'POST',
12353      headers: {
12354        'Content-Type': 'application/json',
12355        'Authorization': `Bearer ${formattedToken}`,
12356        'X-User-Email': userEmail,
12357        'X-API-Request': 'true' // Always include this header for API requests
12358      },
12359      body: JSON.stringify(siteData)
12360    });
12361
12362    console.log('Save response status:', response.status);
12363    
12364    // Process the response
12365    let responseText = await response.text();
12366    console.log('Server response text (first 500 chars):', responseText.substring(0, 500));
12367    let responseData;
12368    
12369    try {
12370      responseData = JSON.parse(responseText);
12371        console.log('Server response parsed successfully:', responseData);
12372        if (responseData.debug) {
12373          console.log('Server debug info:', responseData.debug);
12374          console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys);
12375          console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata);
12376          console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata);
12377          console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata);
12378          console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics);
12379        }
12380    } catch (jsonError) {
12381      if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) {
12382        responseData = { success: true };
12383      } else {
12384        throw new Error('Invalid response format: ' + responseText.substring(0, 100));
12385      }
12386    }
12387
12388    if (!response.ok && !responseData.success) {
12389      // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details
12390      const authSaveFailure = {
12391        status: response.status,
12392        statusText: response.statusText,
12393        error: responseData && responseData.error,
12394        details: responseData && responseData.details,
12395        authMethod: responseData && responseData.authMethod,
12396        fullResponse: responseData,
12397        responseTextPreview: responseText.substring(0, 300)
12398      };
12399      console.error('[AUTH_SAVE] Save request failed:', authSaveFailure);
12400      
12401      // Retry once on 401: token may have expired - try refresh or fresh Magic token
12402      if (response.status === 401) {
12403        console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...');
12404        let newToken = null;
12405        if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') {
12406          newToken = await window.TokenManager.refreshToken();
12407        }
12408        if (!newToken && magic && magic.user) {
12409          try {
12410            newToken = await magic.user.getIdToken();
12411          } catch (e) {
12412            console.warn('[AUTH_SAVE] Magic getIdToken failed:', e);
12413          }
12414        }
12415        if (newToken) {
12416          if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7);
12417          token = newToken;
12418          didToken = newToken;
12419          localStorage.setItem('hydra_auth_token', newToken);
12420          if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12421            window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail);
12422          }
12423          console.log('[AUTH_SAVE] Got fresh token, retrying save...');
12424          const retryResponse = await fetch(saveUrl, {
12425            method: 'POST',
12426            headers: {
12427              'Content-Type': 'application/json',
12428              'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`,
12429              'X-User-Email': userEmail,
12430              'X-API-Request': 'true'
12431            },
12432            body: JSON.stringify(siteData)
12433          });
12434          const retryText = await retryResponse.text();
12435          let retryData;
12436          try { retryData = JSON.parse(retryText); } catch (e) {
12437            if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true };
12438            else throw new Error('Invalid retry response');
12439          }
12440          if (retryResponse.ok || retryData.success) {
12441            response = retryResponse;
12442            responseText = retryText;
12443            responseData = retryData;
12444            console.log('[AUTH_SAVE] Retry succeeded');
12445          } else {
12446            console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData);
12447            throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12448          }
12449        } else {
12450          throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12451        }
12452      } else {
12453        throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12454      }
12455    }
12456
12457    console.log('Configuration saved successfully');
12458
12459    // Update local lastKnownConfigTimestamp after successful save
12460    if (responseData.lastModified) {
12461      window.lastKnownConfigTimestamp = responseData.lastModified;
12462      console.error('[CONFLICT DETECT] ✅ Updated local config timestamp from save response:', window.lastKnownConfigTimestamp);
12463    } else {
12464      console.error('[CONFLICT DETECT] ⚠️ No lastModified in save response, fetching from server...');
12465      // If server doesn't return timestamp, fetch it
12466      try {
12467        const timestampUrl = typeof getApiUrl === 'function' ? 
12468          getApiUrl('/api/config-timestamp') : 
12469          '/api/config-timestamp';
12470        const timestampResponse = await fetch(timestampUrl);
12471        if (timestampResponse.ok) {
12472          const timestampData = await timestampResponse.json();
12473          if (timestampData.lastModified) {
12474            window.lastKnownConfigTimestamp = timestampData.lastModified;
12475            console.error('[CONFLICT DETECT] ✅ Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp);
12476          } else {
12477            console.error('[CONFLICT DETECT] ⚠️ No lastModified in fetched timestamp data:', timestampData);
12478          }
12479        } else {
12480          console.error('[CONFLICT DETECT] ⚠️ Failed to fetch timestamp after save, status:', timestampResponse.status);
12481        }
12482      } catch (err) {
12483        console.error('[CONFLICT DETECT] ⚠️ Could not fetch timestamp after save:', err);
12484      }
12485    }
12486
12487    // Store new token if provided
12488    if (responseData.hydraToken) {
12489      console.log('Received new hydra token from server');
12490      
12491      // Update the global didToken
12492      didToken = responseData.hydraToken.startsWith('hydra:') ? 
12493        responseData.hydraToken.substring(6) : responseData.hydraToken;
12494      
12495      // Update localStorage
12496      localStorage.setItem('hydra_auth_token', didToken);
12497      
12498      // Update TokenManager if available
12499      if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12500        window.TokenManager.storeToken(responseData.hydraToken, userEmail);
12501      }
12502    }
12503
12504    // Process server response data if needed
12505    if (responseData.galleries) {
12506      // If server returns updated galleries data, update the global galleries variable
12507      if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) {
12508        // CRITICAL FIX: Before replacing galleries with server response,
12509        // preserve any gallery settings that might be lost in the response
12510        const updatedGalleries = responseData.galleries.map(serverGallery => {
12511          // Find the matching gallery in our current data
12512          const existingGallery = galleries.find(g => g.id === serverGallery.id);
12513          
12514          // Check if we need to preserve gallery settings
12515          if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) {
12516            // Calculate settings depth
12517            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12518            const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length;
12519            
12520            // If server has fewer settings, merge them with existing settings
12521            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12522              console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12523              
12524              // Create a merged gallery with preserved settings
12525              return {
12526                ...serverGallery,
12527                galleryOptions: {
12528                  ...existingGallery.galleryOptions, // Start with all existing settings
12529                  ...serverGallery.galleryOptions    // Override with any new server settings
12530                }
12531              };
12532            }
12533          }
12534          
12535          // If no preservation needed, use server gallery as is
12536          return serverGallery;
12537        });
12538        
12539        // Update the galleries variable with our preserved data
12540        galleries = updatedGalleries;
12541        console.log(`Updated galleries from server response: ${galleries.length} galleries`);
12542        
12543        // IMPORTANT: Synchronize again after receiving server response
12544        if (typeof synchronizeGalleries === 'function') {
12545          synchronizeGalleries();
12546          console.log('Galleries re-synchronized after server response');
12547        }
12548        
12549        // Keep sidebar UI in sync with current galleries (e.g. after delete)
12550        if (typeof renderGalleries === 'function') {
12551          renderGalleries();
12552        }
12553        
12554        // Update localStorage with full data
12555        try {
12556          localStorage.setItem('galleries', JSON.stringify(galleries));
12557          console.log('Updated localStorage with server gallery data (FULL format)');
12558        } catch (e) {
12559          console.warn('Could not update localStorage with server gallery data', e);
12560        }
12561      }
12562    }
12563
12564    // CRITICAL FIX: Clear all sessionStorage caches after successful save
12565    // This ensures that when users open the site in a new tab, they get fresh data
12566    try {
12567      console.log('Clearing sessionStorage caches after successful save...');
12568      const keysToRemove = [];
12569      for (let i = 0; i < sessionStorage.length; i++) {
12570        const key = sessionStorage.key(i);
12571        if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) {
12572          keysToRemove.push(key);
12573        }
12574      }
12575      keysToRemove.forEach(key => {
12576        sessionStorage.removeItem(key);
12577        console.log('Cleared sessionStorage key: ' + key);
12578      });
12579      console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries');
12580    } catch (cacheError) {
12581      console.warn('Error clearing sessionStorage cache:', cacheError);
12582      // Don't fail the save if cache clearing fails
12583    }
12584
12585    window._saveInProgress = false;
12586
12587    if (!customData || !Array.isArray(customData.sidebarElements)) {
12588      document.dispatchEvent(new CustomEvent('sidebar-save-requested'));
12589    }
12590
12591    // Flush any save queued while this request was in flight
12592    if (window._pendingSaveGalleriesData) {
12593      const pending = window._pendingSaveGalleriesData;
12594      window._pendingSaveGalleriesData = null;
12595      const resolvers = window._pendingSaveResolvers || [];
12596      window._pendingSaveResolvers = [];
12597      return saveGalleries(pending).then(function(result) {
12598        resolvers.forEach(function(r) { r(result); });
12599        return result;
12600      });
12601    }
12602
12603    const idleResolvers = window._pendingSaveResolvers || [];
12604    window._pendingSaveResolvers = [];
12605    idleResolvers.forEach(function(r) { r(true); });
12606
12607    return true;
12608  } catch (error) {
12609    // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401)
12610    console.error('[AUTH_SAVE] Save threw exception:', {
12611      message: error.message,
12612      name: error.name,
12613      stack: error.stack
12614    });
12615    console.error('Error saving configuration:', error);
12616    alert('Failed to save changes: ' + error.message);
12617    window._saveInProgress = false;
12618    const failResolvers = window._pendingSaveResolvers || [];
12619    window._pendingSaveResolvers = [];
12620    failResolvers.forEach(function(r) { r(false); });
12621    return false;
12622  }
12623}
12624
12625// Helper function to get API URL for preview sites
12626function getApiUrl(endpoint) {
12627  // Check if we're on a preview URL
12628  const isPreviewUrl = window.location.hostname === 'preview.neonsky.app';
12629  
12630  if (isPreviewUrl) {
12631    // Extract the site GUID from the path (first segment after domain)
12632    const pathParts = window.location.pathname.split('/').filter(Boolean);
12633    if (pathParts.length > 0) {
12634      const siteGuid = pathParts[0];
12635      // Include the site GUID in the API URL
12636      return `/${siteGuid}${endpoint}`;
12637    }
12638  }
12639  
12640  // Regular case - return the endpoint as is
12641  return endpoint;
12642}
12643
12644function updateActiveStates() {
12645  // Get the most current active gallery ID, ensuring consistency between both variables
12646  const currentActiveId = window.activeGalleryId || activeGalleryId;
12647  
12648  // Ensure both variables are synchronized
12649  window.activeGalleryId = currentActiveId;
12650  activeGalleryId = currentActiveId;
12651  
12652  console.log("updateActiveStates: Using active gallery ID:", currentActiveId);
12653  
12654  // Special case for invisible home page - don't try to highlight it in the menu
12655  const activeGallery = galleries.find(g => g.id === currentActiveId);
12656  if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) {
12657    console.log("Active gallery is invisible home page - not highlighting in menu");
12658    // We're not returning here, so it will still update the visibility classes
12659    // but won't try to find an element to mark as active
12660  }
12661  
12662  // Fix selector issue - look for both .tree and .sidebar
12663  document.querySelectorAll('.tree li, .sidebar li').forEach(li => {
12664    if (!li.dataset.id) return; // Skip items without data-id attribute
12665    
12666    const id = parseInt(li.dataset.id);
12667    if (isNaN(id)) return; // Skip if ID is not a number
12668    
12669    const gallery = galleries.find(g => g.id === id);
12670    
12671    // Update active state using the most current ID
12672    const isActive = id === currentActiveId;
12673    
12674    // Add or remove the active class
12675    if (isActive) {
12676      li.classList.add('active');
12677      console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`);
12678    } else {
12679      li.classList.remove('active');
12680    }
12681    
12682    // Update visibility class if gallery exists
12683    if (gallery) {
12684      li.classList.toggle('hidden-gallery', gallery.visible === false);
12685      
12686      // Update visibility toggle icon if it exists
12687      const toggleButton = li.querySelector('.visibility-toggle');
12688      if (toggleButton) {
12689        toggleButton.classList.toggle('hidden', gallery.visible === false);
12690      }
12691    }
12692  });
12693  
12694  // Verify that the active state was actually set for the current activeGalleryId
12695  const activeElements = document.querySelectorAll('.active');
12696  if (activeElements.length === 0 && currentActiveId) {
12697    // Only log a warning if we're not dealing with an invisible home page
12698    const activeGallery = galleries.find(g => g.id === currentActiveId);
12699    if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) {
12700      console.warn(`No active elements found after update for ID: ${currentActiveId}`);
12701    }
12702  }
12703  
12704  // Log active items for debugging
12705  console.log("Active gallery ID after update:", currentActiveId);
12706  activeElements.forEach(el => 
12707    console.log("Active element:", el.textContent.trim())
12708  );
12709}
12710
12711
12712function debugGalleryStructure() {
12713console.group('Gallery Structure Debug');
12714
12715// Log the raw data structure
12716console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries)));
12717
12718// Log the tree structure
12719const treeStructure = createGalleryTree(galleries);
12720
12721// Helper to print the tree
12722function printTree(items, level = 0) {
12723const indent = '  '.repeat(level);
12724items.forEach(item => {
12725  console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`);
12726  if (item.children && item.children.length > 0) {
12727    printTree(item.children, level + 1);
12728  }
12729});
12730}
12731
12732console.log('Tree structure:');
12733printTree(treeStructure);
12734
12735// Log the DOM structure
12736console.log('DOM structure:');
12737const treeEl = document.getElementById('galleryTree');
12738console.log(treeEl);
12739
12740console.groupEnd();
12741}
12742
12743function createGalleryTree(galleries) {
12744console.log("Creating gallery tree with preservation safeguards...");
12745
12746// First, create a map of galleries by id for quick lookup
12747const galleryMap = {};
12748const processedIds = new Set();
12749
12750// First pass: Create clean copies without children
12751galleries.forEach(gallery => {
12752if (!gallery || !gallery.id) return; // Skip invalid galleries
12753
12754// Create a copy of the gallery object without the children field
12755const cleanGallery = { ...gallery };
12756delete cleanGallery.children;
12757
12758// Initialize an empty children array
12759cleanGallery.children = [];
12760
12761// Add to map - even if we've seen this ID before, keep the latest version
12762if (processedIds.has(gallery.id)) {
12763  console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`);
12764}
12765
12766processedIds.add(gallery.id);
12767galleryMap[gallery.id] = cleanGallery;
12768});
12769
12770// Second pass: Assign children to their parents
12771const rootGalleries = [];
12772
12773// To detect already-processed children
12774const childrenAssigned = new Set();
12775
12776galleries.forEach(gallery => {
12777if (!gallery || !gallery.id) return; // Skip invalid galleries
12778
12779const galleryId = gallery.id;
12780
12781// Skip if this gallery isn't in our map (could be invalid)
12782if (!galleryMap[galleryId]) return;
12783
12784// Skip if we've already assigned this gallery as a child
12785if (childrenAssigned.has(galleryId)) return;
12786
12787if (gallery.parentId && galleryMap[gallery.parentId]) {
12788  // This is a child gallery, but first check it doesn't create a circular reference
12789  
12790  // Find all parents in the chain up to the root
12791  let currentParent = galleryMap[gallery.parentId];
12792  let safeToAdd = true;
12793  let chainIds = [galleryId, gallery.parentId];
12794  
12795  while (currentParent.parentId) {
12796    // If we've seen this ID in the chain, it would create a circular reference
12797    if (chainIds.includes(currentParent.parentId)) {
12798      console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`);
12799      safeToAdd = false;
12800      break;
12801    }
12802    
12803    chainIds.push(currentParent.parentId);
12804    currentParent = galleryMap[currentParent.parentId];
12805    
12806    // If parent doesn't exist in the map, break the chain
12807    if (!currentParent) break;
12808  }
12809  
12810  if (safeToAdd) {
12811    // Safe to add as child
12812    galleryMap[gallery.parentId].children.push(galleryMap[galleryId]);
12813    childrenAssigned.add(galleryId);
12814  } else {
12815    // Not safe - make it a root gallery
12816    rootGalleries.push(galleryMap[galleryId]);
12817  }
12818} else {
12819  // This is a root gallery (no parent or parent doesn't exist)
12820  if (!childrenAssigned.has(galleryId)) {
12821    rootGalleries.push(galleryMap[galleryId]);
12822  }
12823}
12824});
12825
12826// Final verification
12827const allGalleryIds = new Set(galleries.map(g => g.id));
12828const treeGalleryIds = new Set();
12829
12830// Function to collect all IDs in the tree
12831const collectIds = (items) => {
12832items.forEach(item => {
12833  treeGalleryIds.add(item.id);
12834  if (item.children && item.children.length > 0) {
12835    collectIds(item.children);
12836  }
12837});
12838};
12839
12840collectIds(rootGalleries);
12841
12842// Check if any galleries are missing from the tree
12843const missingIds = [];
12844allGalleryIds.forEach(id => {
12845if (!treeGalleryIds.has(id)) {
12846  missingIds.push(id);
12847}
12848});
12849
12850if (missingIds.length > 0) {
12851console.warn(`${missingIds.length} galleries are missing from the tree structure`);
12852
12853// Add the missing galleries as root items
12854missingIds.forEach(id => {
12855  const gallery = galleryMap[id];
12856  if (gallery && !childrenAssigned.has(id)) {
12857    console.log(`Adding missing gallery to root: ${gallery.title}`);
12858    rootGalleries.push(gallery);
12859  }
12860});
12861}
12862
12863console.log(`Created tree with ${rootGalleries.length} root galleries`);
12864return rootGalleries;
12865}
12866
12867function alignFolderStates() {
12868  console.log("Aligning folder data states with collapsed visual appearance");
12869  let changed = 0;
12870  
12871  galleries.forEach(gallery => {
12872    if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) {
12873      gallery.isCollapsed = true;
12874      changed++;
12875    }
12876  });
12877  
12878}
12879
12880
12881
12882function renderGalleryItem(gallery, level = 0, maxLevel = 10) {
12883  // Prevent infinite recursion by limiting depth
12884  if (level >= maxLevel) {
12885    console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`);
12886    return '';
12887  }
12888
12889  // Add debug info to console
12890  console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`);
12891
12892  // Skip hidden items in view mode (but not spacers)
12893  if (!isEditing && gallery.visible === false && !gallery.isSpacer) {
12894    return '';
12895  }
12896  
12897const isPage = isPageMenuItem(gallery);
12898const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu
12899const isNavigableParent = isNavigableMenuParent(gallery);
12900const isSpacer = gallery.isSpacer === true;
12901const isExternal = gallery.isExternal === true;
12902  
12903// CRITICAL FIX: Ensure children are always populated for folders
12904// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array
12905// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode
12906if (isFolder) {
12907  // Always ensure children array exists
12908  if (!gallery.children) {
12909    gallery.children = [];
12910  }
12911  // If children array is empty, build it from flat galleries array using parentId
12912  if (gallery.children.length === 0) {
12913    const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id);
12914    if (childrenFromFlat.length > 0) {
12915      gallery.children = childrenFromFlat;
12916      console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`);
12917    }
12918  }
12919}
12920
12921// Parent-capable items need their chevron and empty nested drop zone immediately,
12922// before the first child has been added.
12923const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0;
12924const hasChildrenClass = hasChildren ? 'has-children' : '';
12925
12926// Standard collapsed state check
12927const isCollapsed = gallery.isCollapsed === true;
12928
12929// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class
12930const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : '';
12931
12932// Add additional classes for special types
12933const spacerClass = isSpacer ? 'spacer-item' : '';
12934const externalClass = isExternal ? 'external-link' : '';
12935const folderClass = isFolder ? 'folder-item' : '';
12936const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : '';
12937  
12938        const duplicateIconSvg = `
12939    <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24">
12940      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
12941        <path d="M16.964 8.982c-.003-2.95-.047-4.478-.906-5.524a4 4 0 0 0-.553-.554C14.4 2 12.76 2 9.48 2s-4.92 0-6.024.905a4 4 0 0 0-.553.554C1.998 4.56 1.998 6.2 1.998 9.48s0 4.92.906 6.023q.25.304.553.553c1.047.86 2.575.904 5.525.906"/>
12942        <path d="m14.028 9.025l2.966-.043m-2.98 13.02l2.966-.043m4.992-7.937l-.028 2.96M9.01 14.036l-.028 2.96m2.505-7.971c-.832.149-2.17.302-2.477 2.024m10.485 10.91c.835-.137 2.174-.27 2.508-1.986M19.495 9.025c.832.149 2.17.302 2.477 2.024M11.5 21.957c-.833-.148-2.17-.301-2.478-2.023"/>
12943      </g>
12944    </svg>
12945  `;
12946
12947
12948  
12949  // NEW: Add nesting level as a data attribute for styling
12950  const nestingLevelAttr = `data-nesting-level="${level}"`;
12951
12952  // Accessible href for SPA items (loadPage/loadGallery still preventDefault)
12953  const menuHref = gallery.isHomePage
12954    ? '/'
12955    : (gallery.url && String(gallery.url).startsWith('/')
12956        ? gallery.url
12957        : (gallery.slug ? ('/' + gallery.slug) : '#'));
12958  const childrenListId = 'gallery-children-' + gallery.id;
12959  const folderExpandedAttr = !isCollapsed ? 'true' : 'false';
12960  const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer)
12961    ? ' aria-current="page"'
12962    : '';
12963
12964  // Create the gallery item HTML with enhanced nesting indicators
12965  const html = `
12966<li data-id="${gallery.id}" 
12967    ${nestingLevelAttr}
12968    class="${gallery.id === activeGalleryId ? 'active' : ''} 
12969          ${gallery.visible === false ? 'hidden-gallery' : ''} 
12970          ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
12971  <div class="gallery-item-content nested-sortable-handle">
12972    ${hasChildren ? `
12973      <button type="button" class="submenu-toggle" onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}" aria-label="Expand or collapse ${String(gallery.title || 'folder').replace(/"/g, '&quot;')}">
12974        <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
12975          <polyline points="9 6 15 12 9 18"></polyline>
12976        </svg>
12977      </button>
12978    ` : ''}
12979    
12980    <!-- Menu item with different rendering based on type -->
12981    ${isSpacer ? `
12982      <!-- Spacer with non-breaking space to ensure height -->
12983      <span class="menu-item spacer" aria-hidden="true">&nbsp;</span>
12984    ` : isExternal ? `
12985      <!-- External URL uses an anchor tag with target="_blank" -->
12986      <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}>
12987        ${gallery.title}
12988      </a>
12989    ` : isFolder ? `
12990      <!-- Folder item with toggle functionality (button for keyboard / SR) -->
12991      <button type="button" class="menu-item folder" data-folder="true"
12992            onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}">
12993        ${gallery.title}
12994      </button>
12995    ` : `
12996      <!-- Real link for SR/keyboard; SPA handlers keep click behavior -->
12997      <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}>
12998        ${gallery.title}
12999      </a>
13000    `}
13001    
13002<div class="controls">
13003      ${!isSpacer ? `
13004      <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)">
13005        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13006          ${gallery.visible !== false ? 
13007            '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>' :
13008            '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>'
13009          }
13010        </svg>
13011      </button>
13012      
13013      <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}">
13014        <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24">
13015          ${gallery.passwordProtected ? 
13016            '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M12 16.5v-2m-7.732 4.345c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345"/><path d="M7.5 9V6.5a4.5 4.5 0 0 1 9 0V9"/></g>' :
13017            '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16.5v-2m-7.732 4.345c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345M7.5 9V6.5A4.5 4.5 0 0 1 12 2c1.96 0 3.5 1.5 4 3" color="currentColor"/>'
13018          }
13019        </svg>
13020      </button>
13021      
13022      <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13023        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13024          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
13025          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
13026        </svg>
13027      </button>
13028      ` : ''}
13029
13030        <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate">
13031         ${duplicateIconSvg}
13032      </button>
13033
13034      
13035      <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)">
13036        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13037          <polyline points="3 6 5 6 21 6"></polyline>
13038          <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
13039        </svg>
13040      </button>
13041${!isSpacer ? `
13042<!-- Add home toggle button -->
13043<button class="btn home-toggle ${gallery.isHomePage ? 'active' : ''}" onclick="toggleHome(${gallery.id}, event)" title="${gallery.isHomePage ? 'Currently set as Home page' : 'Set as Home page'}">
13044  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13045    ${gallery.isHomePage ? 
13046      '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m3.966 5.798l.603-1.401c.501-1.163.751-1.745 1.242-2.07C6.302 2 6.927 2 8.177 2h7.646c1.25 0 1.875 0 2.366.326c.49.326.741.908 1.242 2.07l.603 1.402c.817 1.896 1.225 2.844.788 3.523S19.366 10 17.328 10H6.671c-2.037 0-3.056 0-3.493-.679s-.029-1.627.788-3.523M4.5 10v5c0 1.886 0 2.828.586 3.414c.471.472 1.174.564 2.414.582M19.5 10v5c0 1.886 0 2.828-.586 3.414c-.471.472-1.174.564-2.414.582M13 14l-2.594 3.067c-.317.41-.475.616-.377.775c.098.158.383.158.954.158h2.034c.57 0 .856 0 .954.158c.098.159-.06.364-.377.775L10.984 22M10 2l-1 8m5-8l1 8M4 6h16" color="currentColor"/>' :
13047      '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M12 17h.009M20 8.5v5c0 3.771 0 5.657-1.172 6.828S15.771 21.5 12 21.5s-5.657 0-6.828-1.172S4 17.271 4 13.5v-5"/><path d="m22 10.5l-4.343-4.165C14.99 3.778 13.657 2.5 12 2.5S9.01 3.778 6.343 6.335L2 10.5"/></g>'
13048    }
13049  </svg>
13050</button>
13051` : ''}
13052    </div>
13053  </div>
13054  <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}">
13055    ${gallery.children && gallery.children.length > 0 ? 
13056      gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''}
13057  </ol>
13058</li>
13059`;
13060
13061  return html;
13062}
13063
13064// Additional helper function to add visual indicators during drag operations
13065function addDragVisualFeedback() {
13066  // Add event listener for drag operations
13067  document.addEventListener('dragover', function(e) {
13068    // Find the closest nested sortable list
13069    const nearestList = e.target.closest('.nested-sortable');
13070    if (!nearestList) return;
13071    
13072    // Remove active-drop-target class from all lists
13073    document.querySelectorAll('.active-drop-target').forEach(el => {
13074      el.classList.remove('active-drop-target');
13075    });
13076    
13077    // Add it to the current list
13078    nearestList.classList.add('active-drop-target');
13079    
13080    // Calculate if this would be a nested position
13081    // This is a simplified calculation - SortableJS has its own logic,
13082    // but this helps provide additional visual feedback
13083    const rect = nearestList.getBoundingClientRect();
13084    const distanceFromLeft = e.clientX - rect.left;
13085    
13086    // If we're close to the left edge of a nested list, add a class
13087    if (distanceFromLeft < 30) {
13088      nearestList.classList.add('potential-parent');
13089    } else {
13090      nearestList.classList.remove('potential-parent');
13091    }
13092  });
13093  
13094  // Remove classes when drag ends
13095  document.addEventListener('dragend', function() {
13096    document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => {
13097      el.classList.remove('active-drop-target', 'potential-parent');
13098    });
13099  });
13100}
13101
13102// Make sure the addDragVisualFeedback function is called when the document is ready
13103document.addEventListener('DOMContentLoaded', function() {
13104  // Initialize the visual feedback helper
13105  addDragVisualFeedback();
13106  
13107  // If we're in edit mode, make sure the nested sortables are initialized
13108  if (document.body.classList.contains('edit-mode-active')) {
13109    setTimeout(function() {
13110      initializeNestedSortables();
13111    }, 300);
13112  }
13113});
13114
13115// Update the toggleEditMode function to ensure proper initialization of sortables
13116const originalToggleEditMode = window.toggleEditMode;
13117window.toggleEditMode = function() {
13118    originalToggleEditMode.apply(this, arguments); // Call original function
13119
13120    stopAutoAdvanceTimer();
13121    
13122    const sidebar = document.querySelector('.sidebar');
13123    const editControls = document.querySelector('.edit-controls');
13124
13125    if (currentMenuLayout === 'horizontal') {
13126        if (document.body.classList.contains('edit-mode-active')) {
13127            // In horizontal layout and edit mode is ON: show sidebar and edit controls
13128            if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container
13129            if(editControls) editControls.style.display = 'flex';
13130             // Re-render the sidebar menu for editing
13131            renderGalleries();
13132        } else {
13133            // In horizontal layout and edit mode is OFF: hide sidebar
13134            if(sidebar) sidebar.style.display = 'none';
13135             // Re-render the horizontal menu for viewing
13136            renderHorizontalMenu();
13137        }
13138    } else {
13139        // For other layouts (sidebar, top), ensure sidebar is visible
13140        if(sidebar) sidebar.style.display = 'block'; // Or 'flex'
13141    }
13142};
13143
13144function verifyGalleryStructure() {
13145console.log("Verifying gallery structure integrity...");
13146
13147// Count number of galleries
13148const galleryCount = galleries.length;
13149console.log(`Total galleries in flat array: ${galleryCount}`);
13150
13151// Create tree and count galleries in tree
13152const tree = createGalleryTree([...galleries]);
13153let treeCount = 0;
13154
13155function countGalleriesInTree(items) {
13156items.forEach(item => {
13157  treeCount++;
13158  if (item.children && item.children.length > 0) {
13159    countGalleriesInTree(item.children);
13160  }
13161});
13162}
13163
13164countGalleriesInTree(tree);
13165console.log(`Total galleries in tree structure: ${treeCount}`);
13166
13167if (treeCount !== galleryCount) {
13168console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`);
13169return false;
13170} else {
13171console.log("Gallery structure is consistent");
13172return true;
13173}
13174}
13175
13176function renderGalleries() {
13177console.log("Rendering galleries...");
13178
13179  // Align with the explicit edit toggle only (not admin login / stale sidebar.editing)
13180  if (document.body.classList.contains('edit-mode-active')) {
13181    isEditing = true;
13182  } else if (typeof window.isEditing === 'boolean') {
13183    isEditing = window.isEditing;
13184  }
13185
13186  var galleriesSource = galleries;
13187  if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') {
13188    var helpList = window.HelpSystem.getRenderGalleries();
13189    if (helpList) galleriesSource = helpList;
13190  }
13191
13192try {
13193// Only fix circular references if absolutely necessary
13194const foundCircular = fixCircularReferences();
13195
13196// Only create a new tree if we had to fix circular references
13197let galleryTree;
13198if (foundCircular) {
13199  console.log("Creating fresh gallery tree after fixing circular references");
13200  galleryTree = createGalleryTree(galleriesSource);
13201} else {
13202  console.log("Using existing structure to create gallery tree");
13203  galleryTree = createGalleryTree(galleriesSource);
13204}
13205
13206// Render the tree - IMPORTANT: Add null check
13207const tree = document.getElementById('galleryTree');
13208if (!tree) {
13209  console.warn('galleryTree element not found, cannot render galleries');
13210  return;  // Exit the function if the element doesn't exist
13211}
13212
13213// Render the tree with maximum depth protection
13214tree.innerHTML = `
13215  <ol class="nested-sortable">
13216    ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')}
13217  </ol>
13218`;
13219
13220// Initialize sortable for all nested sortable elements
13221if (isEditing) {
13222  // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables
13223  // This prevents drag issues when items are added and immediately dragged
13224  requestAnimationFrame(() => {
13225    initializeNestedSortables();
13226  });
13227}
13228
13229console.log("Galleries rendered successfully");
13230
13231// Set up folder toggle handler after rendering (works in both view and edit modes)
13232setupFolderToggleHandler();
13233
13234// Verify structure integrity after rendering
13235verifyGalleryStructure();
13236
13237if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
13238} catch (error) {
13239console.error("Error rendering galleries:", error);
13240}
13241}
13242
13243/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */
13244function syncSeoNavA11y() {
13245  const seoNav = document.querySelector('.hydra-seo-nav');
13246  if (!seoNav) return;
13247  const hasVisibleLinks =
13248    document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13249    document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0;
13250  const hideFromAt = hasVisibleLinks;
13251  seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false');
13252  // aria-hidden alone is not enough — focusable descendants must leave the tab order
13253  seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => {
13254    if (hideFromAt) {
13255      if (!el.hasAttribute('data-seo-prev-tabindex')) {
13256        el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : '');
13257      }
13258      el.setAttribute('tabindex', '-1');
13259    } else {
13260      const prev = el.getAttribute('data-seo-prev-tabindex');
13261      if (prev === null) return;
13262      if (prev === '') el.removeAttribute('tabindex');
13263      else el.setAttribute('tabindex', prev);
13264      el.removeAttribute('data-seo-prev-tabindex');
13265    }
13266  });
13267}
13268
13269function getMenuItemHref(galleryItem) {
13270  if (!galleryItem) return '#';
13271  if (galleryItem.isExternal && galleryItem.url) return galleryItem.url;
13272  if (galleryItem.isHomePage) return '/';
13273  if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url;
13274  if (galleryItem.slug) return '/' + galleryItem.slug;
13275  return '#';
13276}
13277
13278
13279function fixCircularReferences() {
13280console.log("Selectively checking for circular references...");
13281
13282// Only process galleries that need checking:
13283// 1. Imported galleries (they're new and might cause issues)
13284// 2. Galleries with extremely deep nesting
13285// 3. Actual circular references
13286
13287const processed = new Set();
13288
13289// First pass: Identify actual circular references without modifying structure
13290const circularIds = new Set();
13291const suspiciousIds = new Set();
13292
13293galleries.forEach(gallery => {
13294if (!gallery || !gallery.id) return;
13295
13296// Skip galleries we've already processed
13297if (processed.has(gallery.id)) return;
13298
13299// Check for self-reference - this is always wrong
13300if (gallery.parentId === gallery.id) {
13301  console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`);
13302  circularIds.add(gallery.id);
13303  return;
13304}
13305
13306// Skip galleries without parents
13307if (!gallery.parentId) {
13308  processed.add(gallery.id);
13309  return;
13310}
13311
13312// Follow the parent chain to detect circular references
13313const parentChain = [gallery.id];
13314let currentId = gallery.parentId;
13315let loopCount = 0;
13316let foundCircular = false;
13317
13318while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops
13319  loopCount++;
13320  
13321  // If we've seen this ID before, it's a circular reference
13322  if (parentChain.includes(currentId)) {
13323    console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`);
13324    circularIds.add(gallery.id);
13325    foundCircular = true;
13326    break;
13327  }
13328  
13329  parentChain.push(currentId);
13330  
13331  // Move up to the next parent
13332  const parent = galleries.find(g => g.id === currentId);
13333  currentId = parent?.parentId;
13334  
13335  // If we can't find the parent, we can stop
13336  if (!parent) break;
13337}
13338
13339// If we hit our loop limit but didn't find a circular reference,
13340// it's suspicious but not definitely circular
13341if (loopCount >= 20 && !foundCircular) {
13342  console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`);
13343  suspiciousIds.add(gallery.id);
13344}
13345
13346// Mark this gallery and all its ancestors as processed
13347parentChain.forEach(id => processed.add(id));
13348});
13349
13350// Second pass: Only fix the identified circular references
13351if (circularIds.size > 0) {
13352console.log(`Fixing ${circularIds.size} confirmed circular references`);
13353
13354galleries.forEach(gallery => {
13355  if (circularIds.has(gallery.id)) {
13356    console.log(`Breaking circular reference for gallery: ${gallery.title}`);
13357    gallery.parentId = null;
13358  }
13359});
13360} else {
13361console.log("No circular references found");
13362}
13363
13364// For suspicious galleries, don't automatically fix them
13365if (suspiciousIds.size > 0) {
13366console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`);
13367}
13368
13369return circularIds.size > 0; // Return true if we fixed anything
13370}
13371
13372// Deep linking implementation that uses the existing slug property
13373
13374// Function to get the slug for a gallery
13375function getGallerySlug(gallery) {
13376// First try to use the existing slug property
13377if (gallery.slug) {
13378console.log(`Using existing gallery slug: ${gallery.slug}`);
13379return gallery.slug;
13380}
13381
13382// If no slug exists, generate one from the title as fallback
13383if (gallery.title) {
13384const generatedSlug = gallery.title
13385  .toLowerCase()
13386  .replace(/[^ws-]/g, '')
13387  .replace(/s+/g, '-')
13388  .replace(/--+/g, '-')
13389  .trim();
13390  
13391console.log(`Generated slug from title: ${generatedSlug}`);
13392return generatedSlug || 'gallery';
13393}
13394
13395// Default fallback
13396console.warn('Gallery has no slug or title, using default');
13397return 'gallery';
13398}
13399
13400// Function to update the URL when a gallery is loaded
13401// Update the URL when a gallery is loaded
13402function updateURLWithGallerySlug(gallery) {
13403  if (!gallery) return;
13404
13405  // Keep bookmarkable /_manage URL — do not rewrite to / or /slug when loading galleries/pages
13406  const __hydraPath = window.location.pathname;
13407  if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') {
13408    console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.');
13409    return;
13410  }
13411
13412  // Get the slug for this gallery/page
13413  let slug = gallery.slug;
13414
13415  if (!slug && gallery.title) {
13416    slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim();
13417  }
13418
13419  // Check if we're in preview mode (add this new code)
13420  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13421  
13422  // Check if current history state already has this galleryId
13423  const currentState = window.history.state;
13424  const currentPath = window.location.pathname;
13425  const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 
13426                               ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug));
13427  
13428  if (isPreviewMode) {
13429    // Extract GUID from current URL path - first segment after domain
13430    const pathParts = window.location.pathname.split('/').filter(Boolean);
13431    const siteGuid = pathParts[0];
13432    
13433    // Create history entry if we have a valid GUID, and it's not already the current state.
13434    // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case.
13435    if (siteGuid && (gallery.isHomePage || slug)) {
13436      const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug);
13437      const alreadyAtPreviewHome = gallery.isHomePage && (
13438        currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/'
13439      );
13440      // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13441      if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) {
13442        console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id);
13443        
13444        // Update browser URL without reloading, preserving the GUID
13445        window.history.pushState(
13446          { galleryId: gallery.id }, 
13447          gallery.title || 'Gallery', 
13448          targetPath
13449        );
13450        
13451        // Update page title
13452        document.title = formatGalleryDocumentTitle(gallery.title);
13453      } else {
13454        console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)');
13455      }
13456    }
13457    
13458    return; // Exit early, we've handled the preview case
13459  }
13460  
13461  // Regular (non-preview) URL handling
13462  if (slug) {
13463    // If this gallery is the designated home page, keep the URL as '/' (canonical home URL)
13464    const targetPath = gallery.isHomePage ? '/' : '/' + slug;
13465    // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13466    if (currentPath !== targetPath || !isAlreadyCurrentPage) {
13467      console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id);
13468      window.history.pushState(
13469        { galleryId: gallery.id }, 
13470        gallery.title || 'Gallery', 
13471        targetPath
13472      );
13473      document.title = formatGalleryDocumentTitle(gallery.title);
13474    } else {
13475      console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update');
13476    }
13477  }
13478}
13479
13480
13481function loadHomePage() {
13482  // Find a gallery marked as home page (regardless of visibility)
13483  const homePage = galleries.find(gallery => gallery.isHomePage === true);
13484  
13485  if (homePage) {
13486    console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`);
13487    
13488    // Update active gallery ID
13489    activeGalleryId = homePage.id;
13490    
13491    // Add a special flag to indicate we're loading the home page
13492    // This allows us to bypass visibility checks
13493    window._loadingHomePage = true;
13494    
13495    // Determine if this is a page or gallery and load appropriately
13496    if (homePage.isPage) {
13497      loadPage(homePage.id);
13498    } else {
13499      loadGallery(homePage.id);
13500    }
13501    
13502    // Clear the flag after loading
13503    setTimeout(() => {
13504      window._loadingHomePage = false;
13505    }, 100);
13506    
13507    // Update UI state
13508    updateActiveStates();
13509    updateMobileTitle();
13510    if (typeof closeMobileMenu === 'function') closeMobileMenu();
13511    
13512    return true;
13513  }
13514  
13515  console.log('No home page defined');
13516  return false;
13517}
13518
13519// 4. Update the handleURLNavigation function to check for home page
13520// Find the existing handleURLNavigation function and modify as follows:
13521// In index.ts
13522function handleURLNavigation() {
13523  const path = window.location.pathname.substring(1); // Remove leading slash
13524  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13525  let slug = path;
13526  let siteGuid = null;
13527
13528  // /_manage is the Hydra editor entry — treat like site root for slug resolution (not a gallery slug)
13529  if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
13530    console.log('[handleURLNavigation] /_manage detected — using root navigation behavior');
13531    slug = '';
13532  }
13533
13534  if (isPreviewMode && path) {
13535    const pathParts = path.split('/');
13536    if (pathParts.length >= 1) {
13537        siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug
13538    }
13539    if (pathParts.length >= 2) {
13540      slug = pathParts[1]; // Slug is the part after GUID
13541      console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`);
13542    } else {
13543      // Only GUID is present, or path is empty after GUID
13544      slug = ''; // Treat as root/home for the given GUID
13545      console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`);
13546    }
13547  }
13548
13549  if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/')
13550    console.log("No specific slug in path (root or GUID-only preview), checking for home page.");
13551    if (!loadHomePage() && galleries.length > 0) {
13552      const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13553      if (firstVisibleGallery) {
13554        console.log("No home page, loading first visible item:", firstVisibleGallery.title);
13555        if (firstVisibleGallery.isPage) {
13556            loadPage(firstVisibleGallery.id);
13557        } else {
13558            loadGallery(firstVisibleGallery.id);
13559        }
13560      } else {
13561        console.log("No home page and no visible items to load for this path.");
13562      }
13563    }
13564  } else {
13565    console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : ""));
13566    // findGalleryByPath should be able to find the item using the slug,
13567    // handling preview mode internally if necessary or by being passed the correct slug.
13568    const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid);
13569
13570    if (matchingGallery) {
13571      console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')');
13572      console.log('🔍 [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements)));
13573      
13574      if (matchingGallery.isSubmenu) {
13575        console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.');
13576        // Potentially, you might want to find and expand its parent here if the UI supports it.
13577      } else {
13578        // Set activeGalleryId *before* calling loadPage/loadGallery
13579        activeGalleryId = matchingGallery.id;
13580        window.activeGalleryId = matchingGallery.id;
13581
13582        // Determine if this is a page by checking multiple indicators
13583        // Check isPage flag, pageId, or pageElements array
13584        const isPage = matchingGallery.isPage === true || 
13585                       (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) ||
13586                       (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0);
13587        
13588        if (isPage) {
13589          console.log('🔍 [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"');
13590          const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
13591          pageLoader(matchingGallery.id);
13592        } else {
13593          console.log('🔍 [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"');
13594          const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
13595          galleryLoader(matchingGallery.id, null, { forceReload: true });
13596        }
13597      }
13598    } else {
13599      console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`);
13600      if (!loadHomePage()) { // Try loading home page
13601        const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13602        if (firstVisibleGallery) {
13603          console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`);
13604           if (firstVisibleGallery.isPage) {
13605               loadPage(firstVisibleGallery.id);
13606           } else {
13607               loadGallery(firstVisibleGallery.id);
13608           }
13609        } else {
13610            console.log("No matching gallery, no home page, and no visible items to load.");
13611            // Optionally, display a 404 message in the gallery-container
13612            const galleryContainer = document.querySelector('.gallery-container');
13613            if (galleryContainer) {
13614                galleryContainer.innerHTML = '<div>Page not found.</div>';
13615                galleryContainer.style.opacity = '1';
13616            }
13617        }
13618      }
13619    }
13620  }
13621
13622  // Update UI states after initiating load.
13623  // These functions should ideally use the now-set activeGalleryId.
13624  if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree
13625  if (typeof updateMobileTitle === 'function') updateMobileTitle();   // For mobile header
13626
13627  // **** ADDED FOR DEEP LINKING HORIZONTAL MENU ****
13628  // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load)
13629  let currentLayout = 'sidebar'; // Default assumption
13630  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
13631    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
13632  } else {
13633    // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings
13634    if (document.body.classList.contains('menu-layout-horizontal')) {
13635        currentLayout = 'horizontal';
13636    }
13637  }
13638
13639  console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`);
13640
13641  if (currentLayout === 'horizontal') {
13642    if (typeof updateActiveStatesHorizontal === 'function') {
13643      console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation.");
13644      // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call.
13645      // A small delay might be needed, or ensure renderHorizontalMenu has run.
13646      // For now, call it directly. If items aren't there, it won't do anything harmful.
13647      setTimeout(() => { // Add a slight delay to allow menu rendering
13648          updateActiveStatesHorizontal();
13649      }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered.
13650    } else {
13651        console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout.");
13652    }
13653  }
13654}
13655
13656/**
13657* Enhanced loadPage function that properly cleans up gallery content
13658* @param {number} id - The page ID
13659* @param {Event} event - Optional event object
13660*/
13661window.loadPage = function(id, event) {
13662  if (event && event.target && event.target.isConnected === false) {
13663    console.log('🔍 [loadPage] Ignoring navigation from detached element');
13664    return;
13665  }
13666  if (event) {
13667    event.preventDefault();
13668    event.stopPropagation();
13669    const now = Date.now();
13670    const lastCallTime = window._lastPageLoadTime || 0;
13671    window._lastPageLoadTime = now;
13672    if (now - lastCallTime < 100) {
13673      console.log('Ignoring duplicate loadPage call');
13674      return;
13675    }
13676  }
13677  console.error('🔍 [loadPage] Loading page with gallery ID:', id);
13678
13679  stopAutoAdvanceTimer();
13680  if (typeof dismissGalleryPasswordOverlay === 'function') {
13681    dismissGalleryPasswordOverlay();
13682  }
13683
13684  let galleriesData = window.galleries || galleries;
13685  const gallery = findGalleryById(galleriesData, id);
13686
13687  if (!gallery) {
13688    console.warn('No gallery found with ID:', id, 'for page load.');
13689    return;
13690  }
13691
13692  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
13693
13694  window.activeGalleryId = id;
13695  activeGalleryId = id;
13696  window._activeContentType = 'page';
13697  if (window.EnhancedGallerySystem) {
13698    window.EnhancedGallerySystem.currentGalleryId = null;
13699  }
13700
13701  if (typeof window.removeGalleryScriptsWithPause === 'function') {
13702    window.removeGalleryScriptsWithPause();
13703  }
13704
13705  const galleryContainer = document.querySelector('.gallery-container');
13706  if (galleryContainer) {
13707    // Clear the container but ensure it's visible
13708    galleryContainer.innerHTML = '';
13709    galleryContainer.style.display = 'block';
13710    galleryContainer.style.visibility = 'visible';
13711    galleryContainer.style.opacity = '1';
13712    console.log('🔍 [loadPage] Cleared and ensured gallery-container is visible');
13713  } else {
13714    console.error('🔍 [loadPage] gallery-container not found!');
13715  }
13716
13717  if (!gallery.pageId) gallery.pageId = `page_${id}`;
13718  if (!gallery.isPage) gallery.isPage = true;
13719
13720  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13721    window.syncGalleryOptionsButtonVisibility();
13722  }
13723
13724  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
13725  window._pageIdToGalleryId[gallery.pageId] = id;
13726
13727  const seedPageElementsOntoManager = function() {
13728    if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
13729      if (window.PageManager && window.PageManager.elements) {
13730        window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
13731      }
13732    } else if (window.PageManager && window.PageManager.elements &&
13733               window.PageManager.elements[gallery.pageId] &&
13734               window.PageManager.elements[gallery.pageId].length > 0) {
13735      gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
13736    }
13737  };
13738  seedPageElementsOntoManager();
13739
13740  const invokePageManagerLoad = function() {
13741    seedPageElementsOntoManager();
13742    try {
13743      console.error('🔍 [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId);
13744      console.error('🔍 [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0);
13745      console.error('🔍 [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0);
13746      console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId]));
13747      
13748      // Small delay to ensure DOM is ready, especially when navigating back via history
13749      setTimeout(() => {
13750        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13751          console.log('🔍 [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId);
13752          return;
13753        }
13754
13755        // Verify container still exists
13756        const containerCheck = document.querySelector('.gallery-container');
13757        console.error('🔍 [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer);
13758        
13759        // PageManager.loadPage is async - we need to properly handle it
13760        try {
13761          const loadPromise = window.PageManager.loadPage(gallery.pageId);
13762          console.error('🔍 [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function');
13763          
13764          if (loadPromise && typeof loadPromise.then === 'function') {
13765            loadPromise.then(() => {
13766              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13767                console.log('🔍 [loadPage] Ignoring stale PageManager completion for:', gallery.pageId);
13768                return;
13769              }
13770
13771              console.error('🔍 [loadPage] PageManager.loadPage promise resolved');
13772              if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13773                window.syncGalleryOptionsButtonVisibility();
13774              }
13775              // Ensure container visibility after PageManager has initialized
13776              if (galleryContainer) {
13777                const pageContainer = galleryContainer.querySelector('.page-container');
13778                if (pageContainer) {
13779                  pageContainer.style.display = 'block';
13780                  pageContainer.style.visibility = 'visible';
13781                  pageContainer.style.opacity = '1';
13782                  const elementCount = pageContainer.querySelectorAll('.page-element').length;
13783                  console.error('🔍 [loadPage] Page container found and made visible, has', elementCount, 'elements');
13784                  if (elementCount === 0) {
13785                    console.error('🔍 [loadPage] WARNING: Page container exists but has no elements!');
13786                    console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]);
13787                  }
13788                } else {
13789                  console.error('❌ [loadPage] Page container not found after PageManager.loadPage');
13790                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
13791                  console.error('❌ [loadPage] Gallery container children:', galleryContainer.children.length);
13792                }
13793              } else {
13794                console.error('❌ [loadPage] Gallery container is null after PageManager.loadPage');
13795              }
13796            }).catch((error) => {
13797              console.error('❌ [loadPage] Error in PageManager.loadPage promise:', error);
13798              console.error('❌ [loadPage] Error stack:', error.stack);
13799            });
13800          } else {
13801            // If loadPage doesn't return a promise, check after a delay
13802            console.error('🔍 [loadPage] loadPage did not return a promise, checking after delay');
13803            setTimeout(() => {
13804              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13805                console.log('🔍 [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId);
13806                return;
13807              }
13808              if (galleryContainer) {
13809                const pageContainer = galleryContainer.querySelector('.page-container');
13810                if (pageContainer) {
13811                  pageContainer.style.display = 'block';
13812                  pageContainer.style.visibility = 'visible';
13813                  pageContainer.style.opacity = '1';
13814                  console.error('🔍 [loadPage] Ensured page-container is visible (non-promise path)');
13815                } else {
13816                  console.error('❌ [loadPage] Page container not found (non-promise path)');
13817                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
13818                }
13819              }
13820            }, 200);
13821          }
13822        } catch (loadError) {
13823          console.error('❌ [loadPage] Exception calling PageManager.loadPage:', loadError);
13824          console.error('❌ [loadPage] Error stack:', loadError.stack);
13825        }
13826        
13827        const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
13828        if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
13829          window.PageManager.setEditMode(isCurrentlyEditing);
13830        }
13831      }, 50); // Small delay to ensure DOM is ready
13832    } catch (error) {
13833      console.error('❌ [loadPage] Error in loadPage function:', error);
13834      console.error('❌ [loadPage] Error stack:', error.stack);
13835    }
13836  };
13837
13838  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
13839    invokePageManagerLoad();
13840  } else {
13841    console.error('❌ [loadPage] PageManager not found yet — waiting for CDN proxy script');
13842    console.error('❌ [loadPage] window.PageManager:', !!window.PageManager);
13843    console.error('❌ [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A');
13844    whenPageManagerReady(function() {
13845      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13846        console.log('🔍 [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId);
13847        return;
13848      }
13849      console.log('✅ [loadPage] PageManager became available — retrying page render');
13850      invokePageManagerLoad();
13851    });
13852  }
13853
13854  // Apply menu visibility
13855  const bodyEl = document.body;
13856  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
13857      bodyEl.classList.add('menu-hidden-on-page');
13858  } else {
13859      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
13860  }
13861  applyMenuOverlayClassForGallery(gallery);
13862
13863  if (typeof updateActiveStates === 'function') updateActiveStates();
13864  if (typeof updateMobileTitle === 'function') updateMobileTitle();
13865  if (typeof closeMobileMenu === 'function') closeMobileMenu();
13866  // Close the gallery options panel when navigating to a different page
13867  if (typeof window.closeOptionsPanel === 'function') {
13868    window.closeOptionsPanel();
13869  }
13870  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
13871  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13872    window.syncGalleryOptionsButtonVisibility();
13873  }
13874  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
13875    window.dispatchEvent(new CustomEvent('page-loaded', {
13876      detail: { pageId: gallery.pageId, galleryId: id }
13877    }));
13878  }
13879}
13880
13881
13882
13883/**
13884* Ensures all required gallery styles are loaded
13885*/
13886function ensureGalleryStylesLoaded() {
13887window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded;
13888// Use Worker route for gallery CSS (avoids CDN SSL issues)
13889const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
13890const requiredStyles = [
13891{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' },
13892{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' }
13893];
13894console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css');
13895
13896requiredStyles.forEach(style => {
13897if (!document.getElementById(style.id)) {
13898  const link = document.createElement('link');
13899  link.id = style.id;
13900  link.rel = 'stylesheet';
13901  link.href = style.href;
13902  if (style.href.includes('cdn.neonsky.app')) {
13903    link.onerror = function() {
13904      console.error('❌ Gallery CSS failed, retrying via proxy:', link.href);
13905      // Set flag if not already set
13906      if (!window.useCdnProxy) {
13907        window.useCdnProxy = true;
13908        console.error('🚩 CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests');
13909      }
13910      const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
13911      link.href = proxyUrl;
13912      link.onerror = function() {
13913        console.error('❌ Proxy failed, trying storage:', proxyUrl);
13914        link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
13915      };
13916    };
13917  }
13918  document.head.appendChild(link);
13919  console.log(`Added gallery style: ${style.id}`);
13920}
13921});
13922}
13923
13924
13925/**
13926* Ensures all required gallery scripts are loaded
13927* @returns {Promise} A promise that resolves when all scripts are loaded
13928*/
13929function ensureGalleryScriptsLoaded() {
13930const requiredScripts = [
13931{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' },
13932{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' },
13933          { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' }
13934];
13935
13936const promises = requiredScripts.map(script => {
13937return new Promise((resolve, reject) => {
13938  // If script is already loaded, resolve immediately
13939  if (document.getElementById(script.id)) {
13940    resolve();
13941    return;
13942  }
13943  
13944  // Create and load the script
13945  const scriptElement = document.createElement('script');
13946  scriptElement.id = script.id;
13947  scriptElement.src = script.src;
13948  scriptElement.onload = resolve;
13949  scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`));
13950  document.head.appendChild(scriptElement);
13951});
13952});
13953
13954return Promise.all(promises);
13955}
13956
13957/**
13958 * Creates a clean gallery context without using an iframe
13959 * @param {Object} gallery - The gallery object
13960 * @param {HTMLElement} container - The container to load the gallery into
13961 */
13962function createGalleryContext(gallery, container) {
13963  console.log('Creating gallery context for:', gallery.title);
13964
13965  // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script
13966  // This ensures styles are available even if NDJSON fails and falls back to classic JSON
13967  ensureGalleryStylesLoaded();
13968
13969  // Generate the page alias safely
13970  const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-');
13971
13972  // Get our gallery options
13973  const galleryOptions = gallery.galleryOptions || {};
13974
13975  // Get token data
13976  let authToken = null;
13977  // First try TokenManager if available
13978  if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
13979    authToken = window.TokenManager.getToken();
13980  } 
13981  // Then try global token
13982  else if (window.didToken) {
13983    authToken = window.didToken;
13984  }
13985
13986  // Set up Parameters for the gallery
13987  const galleryParameters = {
13988    SiteAlias: window.location.hostname,
13989    InitialPageUuid: gallery.pageId,
13990    InitialPageAlias: pageAlias,
13991    isInEditor: window.isEditing || false,
13992    siteId: window.siteId || '',
13993    isHydra: true
13994  };
13995
13996  // If we have a token, add it
13997  if (authToken) {
13998    galleryParameters.hydraAuthToken = authToken;
13999  }
14000
14001  // Preserve original window.Parameters
14002  const originalParameters = window.Parameters;
14003
14004  // REMOVED: Loading indicator - no longer showing the loading animation
14005
14006  // Load the main gallery script - Use Worker route (avoids CDN SSL issues)
14007  const galleryScript = document.createElement('script');
14008  const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14009  galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js';
14010  console.log('Loading gallery-main script from Worker:', galleryScript.src);
14011  galleryScript.onerror = function() {
14012    console.error('❌ Gallery script failed, retrying via proxy:', galleryScript.src);
14013    const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14014    galleryScript.src = proxyUrl;
14015    galleryScript.onerror = function() {
14016      console.error('❌ Proxy failed, trying storage:', proxyUrl);
14017      galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14018    };
14019  };
14020
14021  // Setup the gallery configuration
14022  const neonGalleryConfig = {
14023    useData: true,
14024    useCDN: true,
14025    version: 'live',
14026    manualCollectionName: "mod",
14027    layoutType: "grid",
14028    ...galleryOptions,
14029    siteId: window.siteId || ''
14030  };
14031
14032  // Execute in a safe way that minimizes global namespace pollution
14033  try {
14034    // Set up the global variables needed by the gallery script
14035    window.Parameters = galleryParameters;
14036    window.neonGalleryConfig = neonGalleryConfig;
14037
14038    // Monitor for script load/error
14039    galleryScript.onload = () => {
14040      console.log('Gallery script loaded successfully for:', gallery.title);
14041      
14042      // REMOVED: Don't need to remove the loading indicator since we don't add it
14043    };
14044
14045    galleryScript.onerror = () => {
14046      console.error('Failed to load gallery script for:', gallery.title);
14047      //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>';
14048      // Restore original parameters
14049      window.Parameters = originalParameters;
14050    };
14051
14052    // Add the script to load the gallery
14053    document.body.appendChild(galleryScript);
14054
14055    // Set up a cleanup function that will be called when another gallery is loaded
14056    container.cleanup = () => {
14057      // Restore original parameters when cleaning up
14058      window.Parameters = originalParameters;
14059      
14060      // Remove the gallery script to prevent conflicts
14061      galleryScript.remove();
14062      
14063      // Clear main gallery container
14064      if (container.parentNode) {
14065        container.innerHTML = '';
14066      }
14067    };
14068  } catch (error) {
14069    console.error('Error creating gallery context:', error);
14070    //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>';
14071    // Restore original parameters
14072    window.Parameters = originalParameters;
14073  }
14074}
14075
14076/**
14077* Only used for the PageManager's initializeDOM - ensures clean content swap
14078*/
14079function cleanGalleryContainer() {
14080const galleryContainer = document.querySelector('.gallery-container');
14081if (galleryContainer) {
14082galleryContainer.innerHTML = '';
14083}
14084}
14085
14086
14087
14088/**
14089* Initializes the page using the PageManager
14090* To be used by PageManager.initializeDOM
14091*/
14092function initPageContent(pageId) {
14093// First clear the container completely
14094cleanGalleryContainer();
14095
14096// Then load the page content
14097if (window.PageManager && window.PageManager.loadPage) {
14098window.PageManager.loadPage(pageId);
14099}
14100}
14101
14102
14103/**
14104* Cleanup gallery content when changing between galleries or pages
14105*/
14106function cleanupGalleryContent() {
14107// Get the gallery container
14108const galleryContainer = document.querySelector('.gallery-container');
14109if (!galleryContainer) return;
14110
14111// Find the direct gallery content
14112const directContent = galleryContainer.querySelector('.gallery-direct-content');
14113if (directContent && typeof directContent.cleanup === 'function') {
14114// Call the cleanup function
14115directContent.cleanup();
14116}
14117
14118// Clear the container
14119galleryContainer.innerHTML = '';
14120}
14121
14122
14123
14124
14125/**
14126* Simplified loadGallery function that follows the same content swap pattern
14127* @param {number} id - The gallery ID
14128* @param {Event} event - Optional event object
14129*/
14130
14131// Override loadGallery to update URL
14132window.loadGallery = async function(id, event) {
14133
14134
14135// Stop event propagation if provided
14136if (event) {
14137  event.stopPropagation();
14138}
14139
14140const galleryContainer = document.querySelector('.gallery-container');
14141if (galleryContainer) {
14142  // Apply immediate hiding styles
14143  galleryContainer.style.opacity = '0';
14144}
14145
14146console.log('Loading gallery with ID:', id);
14147
14148stopAutoAdvanceTimer();
14149if (typeof dismissGalleryPasswordOverlay === 'function') {
14150  dismissGalleryPasswordOverlay();
14151}
14152
14153// Find gallery by ID
14154const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
14155
14156if (!gallery) {
14157  console.warn('No gallery found with ID:', id);
14158  if (galleryContainer) {
14159  galleryContainer.style.opacity = '1';
14160  } // Show container if gallery not found
14161  return;
14162}
14163
14164// Check if this is a page, if so use loadPage instead
14165if (isPageMenuItem(gallery)) {
14166  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
14167  return loadPage(id, event);
14168}
14169
14170// Skip submenu items
14171if (gallery.isSubmenu) {
14172  console.log('Gallery is submenu, skipping URL update');
14173  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
14174  if (galleryContainer) {
14175  galleryContainer.style.opacity = '1';
14176  } // Show container
14177  return;
14178}
14179
14180console.log('Found gallery:', gallery.title);
14181
14182window._activeContentType = 'gallery';
14183beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
14184if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14185  window.PageManager.clearCurrentPage();
14186}
14187
14188// CRITICAL: Set active gallery ID consistently in both global and window scope
14189console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
14190window.activeGalleryId = id;
14191activeGalleryId = id; // Ensure both variables are synchronized
14192
14193// Save reference to which gallery we're loading (for later verification)
14194window.currentLoadingGalleryId = id;
14195
14196// CRITICAL: Thorough cleanup with await to ensure completion
14197if (typeof window.removeGalleryScriptsWithPause === 'function') {
14198  await window.removeGalleryScriptsWithPause();
14199}
14200
14201
14202// Clear all existing content from gallery container
14203if (!galleryContainer) {
14204  console.error('Gallery container not found');
14205  return;
14206}
14207
14208// Force clear all content
14209galleryContainer.innerHTML = '';
14210
14211
14212// CRITICAL: Double-check active gallery ID before updating UI
14213if (window.activeGalleryId !== id || activeGalleryId !== id) {
14214  console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
14215  window.activeGalleryId = id;
14216  activeGalleryId = id;
14217}
14218
14219// Apply menu visibility based on the gallery's setting and edit mode
14220const bodyEl = document.body;
14221if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14222    bodyEl.classList.add('menu-hidden-on-page');
14223} else {
14224    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14225}
14226applyMenuOverlayClassForGallery(gallery);
14227
14228// Update UI state using explicit window function references or local fallbacks
14229if (typeof window.updateActiveStates === 'function') {
14230  window.updateActiveStates();
14231} else if (typeof updateActiveStates === 'function') {
14232  updateActiveStates();
14233}
14234
14235if (typeof window.updateMobileTitle === 'function') {
14236  window.updateMobileTitle();
14237} else if (typeof updateMobileTitle === 'function') {
14238  updateMobileTitle();
14239}
14240
14241if (typeof window.closeMobileMenu === 'function') {
14242  window.closeMobileMenu();
14243} else if (typeof closeMobileMenu === 'function') {
14244  closeMobileMenu();
14245}
14246
14247// Update URL with gallery slug
14248if (typeof window.updateURLWithGallerySlug === 'function') {
14249  window.updateURLWithGallerySlug(gallery);
14250} else if (typeof updateURLWithGallerySlug === 'function') {
14251  updateURLWithGallerySlug(gallery);
14252}
14253
14254  // Create gallery container and load content
14255const neonGalleryContainer = document.createElement('div');
14256neonGalleryContainer.id = 'neon-gallery-container';
14257neonGalleryContainer.className = 'gallery-direct-content';
14258neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
14259neonGalleryContainer.style.width = '100%';
14260neonGalleryContainer.style.height = '100%';
14261galleryContainer.appendChild(neonGalleryContainer);
14262
14263// Add a cache-busting parameter to ensure script is freshly loaded
14264const timestamp = Date.now();
14265
14266// Set up gallery parameters
14267window.Parameters = {
14268  SiteAlias: window.location.hostname,
14269  InitialPageUuid: gallery.pageId,
14270  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
14271  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
14272  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14273  isHydra: true,
14274  galleryInstanceId: timestamp,
14275  // Added loadedGalleryId as per your original function
14276  loadedGalleryId: gallery.id
14277};
14278
14279// Debug: Check what galleryOptions contains
14280console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
14281
14282// Set up gallery config
14283window.neonGalleryConfig = {
14284  useData: true,
14285  useCDN: true,
14286  version: 'live',
14287  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
14288  layoutType: gallery.galleryOptions?.layoutType || "grid",
14289  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
14290  // This ensures that only properties from the current gallery are included, not from previous galleries
14291  ...(gallery.galleryOptions || {}),
14292  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
14293  // These fields are required for the gallery to determine if it should use Load Network racing
14294  loadTxId: gallery.galleryOptions?.loadTxId || null,
14295  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
14296  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
14297  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
14298  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
14299  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
14300  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
14301  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
14302  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14303  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
14304  galleryId: gallery.id,
14305  galleryInstanceId: timestamp
14306};
14307
14308console.error('🔍 INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:', window.neonGalleryConfig.isPerma, 'permaURL:', window.neonGalleryConfig.permaURL, 'loadTxId:', window.neonGalleryConfig.loadTxId, 'loadPermaURL:', window.neonGalleryConfig.loadPermaURL, 'cdnNdjsonId:', window.neonGalleryConfig.cdnNdjsonId);
14309
14310// Create and add the gallery script with cache-busting
14311// Use Worker route (avoids CDN SSL issues)
14312const script = document.createElement('script');
14313const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14314script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
14315script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
14316script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
14317console.log('Loading gallery-main script from Worker:', script.src);
14318script.onerror = function() {
14319  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
14320  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14321  script.src = proxyUrl;
14322  script.onerror = function() {
14323    console.error('❌ Proxy failed, trying storage:', proxyUrl);
14324    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14325  };
14326};
14327
14328// Added script.onload from your original function
14329script.onload = function() {
14330  if (window.currentLoadingGalleryId !== gallery.id) {
14331    console.warn('Gallery ID mismatch! Expected:', gallery.id,
14332                 'Current:', window.currentLoadingGalleryId);
14333  }
14334  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
14335};
14336
14337
14338if (galleryContainer) {
14339    setTimeout(() => {
14340      galleryContainer.style.opacity = '1';
14341    }, 50); // Small delay to ensure content is ready
14342  }
14343
14344console.log(`Loading fresh gallery script for: ${gallery.title}`);
14345document.body.appendChild(script);
14346}
14347
14348// Function to ensure gallery container is visible
14349function ensureGalleryVisibility() {
14350  const galleryContainer = document.querySelector('.gallery-container');
14351  if (galleryContainer && galleryContainer.style.opacity === '0') {
14352    console.log('ensureGalleryVisibility: Setting gallery container opacity to 1');
14353    galleryContainer.style.opacity = '1';
14354  }
14355}
14356
14357// Handle browser back/forward navigation
14358window.addEventListener('popstate', async function(event) { // Make it async
14359  if (window.__hydraPopstateLock) {
14360    return;
14361  }
14362  window.__hydraPopstateLock = true;
14363  try {
14364  console.error('🔍 [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname);
14365  let galleryIdToLoad = null;
14366  let galleryToLoad = null;
14367
14368  // First, try to get galleryId from event.state
14369  if (event.state && event.state.galleryId) {
14370    galleryIdToLoad = event.state.galleryId;
14371    console.error('🔍 [Popstate] Found galleryId in event.state:', galleryIdToLoad);
14372    // Ensure galleries array is available and use findGalleryById helper
14373    const currentGalleries = window.galleries || galleries || [];
14374    galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad);
14375  }
14376
14377  if (window.__closingNestedLightbox) {
14378    window.__closingNestedLightbox = false;
14379    if (typeof updateActiveStates === 'function') updateActiveStates();
14380    if (typeof updateMobileTitle === 'function') updateMobileTitle();
14381    return;
14382  }
14383
14384  if (window.lightboxNavigationStack?.length > 0 &&
14385      typeof window.closeNestedGalleryLightbox === 'function') {
14386    const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1];
14387    const parentId = stackTop?.parentGalleryId;
14388    if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) {
14389      window.closeNestedGalleryLightbox({ fromPopstate: true });
14390      if (typeof updateActiveStates === 'function') updateActiveStates();
14391      if (typeof updateMobileTitle === 'function') updateMobileTitle();
14392      return;
14393    }
14394  }
14395
14396  if (typeof dismissGalleryPasswordOverlay === 'function') {
14397    dismissGalleryPasswordOverlay();
14398  }
14399
14400  // If no gallery found from state, resolve from URL slug (handles history entries with null state)
14401  if (!galleryToLoad) {
14402    console.error('🔍 [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname);
14403    const path = window.location.pathname.substring(1);
14404    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
14405    let slug = path;
14406    let siteGuid = null;
14407    if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
14408      slug = '';
14409    }
14410    if (isPreviewMode && path) {
14411      const pathParts = path.split('/');
14412      if (pathParts.length >= 1) siteGuid = pathParts[0];
14413      if (pathParts.length >= 2) slug = pathParts[1];
14414      else slug = '';
14415    }
14416    if (slug && typeof findGalleryByPath === 'function') {
14417      galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid);
14418      if (galleryToLoad) {
14419        console.error('🔍 [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14420      }
14421    }
14422  }
14423
14424  // If we have a gallery from state or URL, load it
14425  let contentLoaded = false;
14426  if (galleryToLoad) {
14427    console.error('🔍 [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14428    console.error('🔍 [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements)));
14429    
14430    activeGalleryId = galleryToLoad.id;
14431    window.activeGalleryId = galleryToLoad.id;
14432
14433    // Browser history navigation is explicit user intent — load content regardless of menu visibility.
14434    // Determine if this is a page by checking multiple indicators
14435    const isPage = galleryToLoad.isPage === true || 
14436                   (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) ||
14437                   (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0);
14438    
14439    if (isPage) {
14440      console.error('🔍 [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"');
14441      const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
14442      if (typeof pageLoader === 'function') {
14443          console.error('🔍 [Popstate] Calling loadPage for ID:', galleryToLoad.id);
14444          pageLoader(galleryToLoad.id);
14445          contentLoaded = true;
14446      } else {
14447          console.error('❌ [Popstate] loadPage function not available');
14448      }
14449    } else {
14450      console.error('🔍 [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"');
14451      // Clear page state left over from prior page navigation
14452      if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14453        window.PageManager.clearCurrentPage();
14454      }
14455      window._activeContentType = 'gallery';
14456      const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
14457      if (typeof galleryLoader === 'function') {
14458          await galleryLoader(galleryToLoad.id, null, { forceReload: true });
14459          contentLoaded = true;
14460      } else {
14461          console.error('❌ [Popstate] loadGallery function not available');
14462      }
14463    }
14464  } else {
14465    console.error('❌ [Popstate] No gallery found and handleURLNavigation not available');
14466  }
14467
14468  // Update UI states after loading (or attempting to load) content
14469  if (typeof updateActiveStates === 'function') updateActiveStates();
14470  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14471  
14472  // Ensure gallery visibility after popstate navigation
14473  setTimeout(ensureGalleryVisibility, 1000);
14474
14475  let currentLayout = 'sidebar';
14476  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
14477    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
14478  } else if (document.body.classList.contains('menu-layout-horizontal')) {
14479    currentLayout = 'horizontal';
14480  }
14481
14482  if (currentLayout === 'horizontal') {
14483    if (typeof updateActiveStatesHorizontal === 'function') {
14484      console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay.");
14485      setTimeout(() => {
14486          console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal.");
14487          updateActiveStatesHorizontal();
14488      }, 150); // Slightly longer delay for popstate, as full content might be re-initializing
14489    }
14490  }
14491  } finally {
14492    setTimeout(function() { window.__hydraPopstateLock = false; }, 300);
14493  }
14494});
14495
14496// Check for direct URL access on page load (fallback when bootstrap did not route)
14497document.addEventListener('DOMContentLoaded', function() {
14498setTimeout(function() {
14499console.log('Checking for deep links after DOM content loaded');
14500var __p = window.location.pathname;
14501if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14502  handleURLNavigation();
14503}
14504// Ensure gallery visibility after URL navigation
14505setTimeout(ensureGalleryVisibility, 1000);
14506}, 500);
14507});
14508
14509document.addEventListener('page-save-requested', function(e) {
14510const data = e.detail;
14511if (data && data.pageId && data.elements) {
14512// Find the gallery/page that corresponds to this pageId
14513const page = galleries.find(g => g.pageId === data.pageId);
14514if (page) {
14515  // Store the page elements
14516  page.pageElements = data.elements;
14517  
14518  // Save to server
14519  saveGalleries();
14520}
14521}
14522});
14523
14524document.addEventListener('DOMContentLoaded', function() {
14525function preloadRichTextEditor() {
14526  if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') {
14527    console.log('Preloading Rich Text Editor dependencies');
14528    return window.RichTextEditor._ensureEditorDependencies()
14529      .then(() => console.log('Rich Text Editor dependencies preloaded successfully'))
14530      .catch(error => console.warn('Failed to preload Rich Text Editor:', error));
14531  }
14532
14533  if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') {
14534    return window.PageManager._ensureRichTextEditor()
14535      .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error));
14536  }
14537
14538  return null;
14539}
14540
14541preloadRichTextEditor();
14542setTimeout(preloadRichTextEditor, 1000);
14543setTimeout(preloadRichTextEditor, 3000);
14544});
14545
14546document.addEventListener('DOMContentLoaded', function() {
14547const submenuCheck = document.getElementById('createSubmenu');
14548const submenuTitleField = document.getElementById('submenuTitle');
14549const submenuTitleGroup = submenuTitleField?.parentElement;
14550
14551if (submenuCheck && submenuTitleGroup) {
14552submenuCheck.addEventListener('change', function() {
14553  submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
14554});
14555}
14556});
14557
14558// Also check immediately if document is already loaded (fallback only — avoid duplicate loadPage vs bootstrap)
14559if (document.readyState !== 'loading') {
14560setTimeout(function() {
14561console.log('Document already loaded, checking for deep links');
14562var __p2 = window.location.pathname;
14563if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14564  handleURLNavigation();
14565}
14566}, 300);
14567}
14568
14569// ============================================
14570// CONCURRENT EDIT CONFLICT DETECTION
14571// ============================================
14572(function() {
14573  // Initialize local timestamp tracking
14574  window.lastKnownConfigTimestamp = null;
14575  let conflictPollInterval = null;
14576  
14577  // Initialize timestamp on page load
14578  async function initializeConfigTimestamp() {
14579    try {
14580      const timestampUrl = typeof getApiUrl === 'function' ? 
14581        getApiUrl('/api/config-timestamp') : 
14582        '/api/config-timestamp';
14583      console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl);
14584      const response = await fetch(timestampUrl);
14585      if (response.ok) {
14586        const data = await response.json();
14587        if (data.lastModified) {
14588          window.lastKnownConfigTimestamp = data.lastModified;
14589          console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp);
14590        } else {
14591          console.error('[CONFLICT DETECT] No lastModified in response:', data);
14592        }
14593      } else {
14594        console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status);
14595      }
14596    } catch (error) {
14597      console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error);
14598    }
14599  }
14600  
14601  // Check for conflicts by polling server
14602  async function checkForConflicts() {
14603    // Only poll when in edit mode
14604    if (!document.body.classList.contains('edit-mode-active')) {
14605      console.error('[CONFLICT DETECT] Skipping poll - not in edit mode');
14606      return;
14607    }
14608    
14609    try {
14610      const timestampUrl = typeof getApiUrl === 'function' ? 
14611        getApiUrl('/api/config-timestamp') : 
14612        '/api/config-timestamp';
14613      console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl);
14614      console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp);
14615      
14616      const response = await fetch(timestampUrl);
14617      
14618      if (response.ok) {
14619        const data = await response.json();
14620        const serverTimestamp = data.lastModified;
14621        console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp);
14622        
14623        // If we have a local timestamp and it differs from server, show conflict
14624        if (window.lastKnownConfigTimestamp !== null && 
14625            serverTimestamp !== null && 
14626            serverTimestamp !== window.lastKnownConfigTimestamp) {
14627          console.error('[CONFLICT DETECT] ⚠️ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp);
14628          showConflictIndicator();
14629        } else {
14630          console.error('[CONFLICT DETECT] No conflict - timestamps match or null');
14631          hideConflictIndicator();
14632        }
14633      } else {
14634        console.error('[CONFLICT DETECT] Poll failed with status:', response.status);
14635      }
14636    } catch (error) {
14637      console.error('[CONFLICT DETECT] Error checking for conflicts:', error);
14638    }
14639  }
14640  
14641  // Show conflict indicator icon
14642  function showConflictIndicator() {
14643    const indicator = document.getElementById('conflictIndicator');
14644    console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator);
14645    if (indicator) {
14646      indicator.style.display = 'flex';
14647      indicator.style.visibility = 'visible';
14648      indicator.style.opacity = '1';
14649      indicator.style.background = '#d32f2f';
14650      indicator.style.borderColor = '#d32f2f';
14651      console.error('[CONFLICT DETECT] Conflict indicator displayed');
14652    } else {
14653      console.error('[CONFLICT DETECT] ⚠️ Conflict indicator element not found in DOM!');
14654    }
14655  }
14656  
14657  // Hide conflict indicator icon
14658  function hideConflictIndicator() {
14659    const indicator = document.getElementById('conflictIndicator');
14660    if (indicator) {
14661      indicator.style.display = 'none';
14662      indicator.style.visibility = 'hidden';
14663      indicator.style.opacity = '0';
14664    }
14665  }
14666  
14667  // Start/stop polling based on edit mode
14668  function updatePolling() {
14669    const isEditMode = document.body.classList.contains('edit-mode-active');
14670    
14671    // Always hide indicator when not in edit mode
14672    if (!isEditMode) {
14673      hideConflictIndicator();
14674    }
14675    
14676    if (isEditMode && !conflictPollInterval) {
14677      // Start polling every 10 seconds
14678      conflictPollInterval = setInterval(checkForConflicts, 10000);
14679      console.error('[CONFLICT DETECT] ✅ Started conflict detection polling (every 10s)');
14680      
14681      // Also check immediately
14682      checkForConflicts();
14683    } else if (!isEditMode && conflictPollInterval) {
14684      // Stop polling when not in edit mode
14685      clearInterval(conflictPollInterval);
14686      conflictPollInterval = null;
14687      hideConflictIndicator();
14688      console.error('[CONFLICT DETECT] Stopped conflict detection polling');
14689    }
14690  }
14691  
14692  // Initialize on page load
14693  if (document.readyState === 'loading') {
14694    document.addEventListener('DOMContentLoaded', function() {
14695      initializeConfigTimestamp();
14696      // Watch for edit mode changes
14697      const editModeObserver = new MutationObserver(updatePolling);
14698      editModeObserver.observe(document.body, { 
14699        attributes: true, 
14700        attributeFilter: ['class'] 
14701      });
14702      updatePolling();
14703    });
14704  } else {
14705    initializeConfigTimestamp();
14706    // Watch for edit mode changes
14707    const editModeObserver = new MutationObserver(updatePolling);
14708    editModeObserver.observe(document.body, { 
14709      attributes: true, 
14710      attributeFilter: ['class'] 
14711    });
14712    updatePolling();
14713  }
14714})();
14715
14716</script>
14716
14717</head>
14718<body class="menu-layout-sidebar">
14719  <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a>
14720<nav class="hydra-seo-nav" aria-label="Site navigation">
14721  <ul>
14722    <li><a href="/about-the-book">About the book</a></li>
14723    <li><a href="/podcast">Podcast</a></li>
14724    <li><a href="/always-take-notes-book">Always Take Notes book</a></li>
14725    <li><a href="/longform-journalism">Longform journalism</a></li>
14726    <li><a href="/about">About</a></li>
14727    <li><a href="/contact">Contact</a></li>
14728  </ul>
14729</nav>
14730
14731
14732  <div class="global-header-controls">
14733    
14734    <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle">
14735       <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14736        <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
14737          <path d="m16.214 4.982l1.402-1.401a1.982 1.982 0 0 1 2.803 2.803l-1.401 1.402m-2.804-2.804l-5.234 5.234c-1.045 1.046-1.568 1.568-1.924 2.205S8.342 14.561 8 16c1.438-.342 2.942-.7 3.579-1.056s1.16-.879 2.205-1.924l5.234-5.234m-2.804-2.804l2.804 2.804"/>
14738          <path d="M21 12c0 4.243 0 6.364-1.318 7.682S16.242 21 12 21s-6.364 0-7.682-1.318S3 16.242 3 12s0-6.364 1.318-7.682S7.758 3 12 3"/>
14739        </g>
14740      </svg>
14741    </button>
14742    <button id="conflictIndicator" class="btn-icon btn-primary" onclick="window.location.reload()" style="display: none;" data-custom-tooltip="Changes detected in another window" data-tooltip-position="right">
14743      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;">
14744        <path fill="currentColor" d="M2 12c0 5 4 9 9 9c2.4 0 4.7-.9 6.4-2.6l-1.5-1.5c-1.3 1.4-3 2.1-4.9 2.1c-6.2 0-9.4-7.5-4.9-11.9S18 5.8 18 12h-3l4 4h.1l3.9-4h-3c0-5-4-9-9-9s-9 4-9 9m8 3h2v2h-2zm0-8h2v6h-2z"/>
14745      </svg>
14746    </button>
14747  </div>
14748
14749  <div class="container">
14750    <div class="mobile-header">
14751      <div class="mobile-header-content">
14752        <div class="horizontal-header-logo"></div>
14753        <div class="horizontal-menu-container"></div>
14754      </div>
14755      <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar">
14756        <span class="hamburger-icon" aria-hidden="true">
14757          <span class="hamburger-line"></span>
14758          <span class="hamburger-line"></span>
14759          <span class="hamburger-line"></span>
14760        </span>
14761      </button>
14762    </div>
14763    <div id="mobileBackdrop" class="mobile-backdrop"></div>
14764
14765    <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site">
14766      <div class="sidebar-header">
14767</div>
14768
14769  
14770  <div class="edit-controls" data-help-id="edit-toolbar">
14771  <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar">
14772    <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
14773      <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.195 8.765A4 4 0 1 1 12 14v1m.05 4v.1h-.1V19z" />
14774    </svg>
14775  </button>
14776  <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar">
14777    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14778      <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m19 12.13l-6.061 6.077c-1.783 1.788-2.675 2.682-3.77 2.78q-.27.025-.543 0c-1.094-.098-1.986-.992-3.769-2.78l-2.02-2.026a2.87 2.87 0 0 1 0-4.052m16.163 0l-8.082-8.103M19 12.129H2.837m8.081-8.103l-8.081 8.103m8.081-8.103L8.898 2M22 20a2 2 0 1 1-4 0c0-1.105 2-3 2-3s2 1.895 2 3" color="currentColor"/>
14779    </svg>
14780  </button>
14781  <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add">
14782    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14783      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
14784        <path d="M13 2h.273c3.26 0 4.892 0 6.024.798c.324.228.612.5.855.805c.848 1.066.848 2.6.848 5.67v2.545c0 2.963 0 4.445-.469 5.628c-.754 1.903-2.348 3.403-4.37 4.113c-1.257.441-2.83.441-5.98.441c-1.798 0-2.698 0-3.416-.252c-1.155-.406-2.066-1.263-2.497-2.35C4 18.722 4 17.875 4 16.182V12"/>
14785        <path d="M21 12a3.333 3.333 0 0 1-3.333 3.333c-.666 0-1.451-.116-2.098.057a1.67 1.67 0 0 0-1.179 1.179c-.173.647-.057 1.432-.057 2.098A3.333 3.333 0 0 1 11 22m0-16H3m4-4v8"/>
14786      </g>
14787    </svg>
14788  </button>
14789  <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm(
14789)" data-custom-tooltip="Add Element">
14790    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14791      <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 2v8m4-4h-8M2 6c0-1.4 0-2.1.272-2.635a2.5 2.5 0 0 1 1.093-1.093C3.9 2 4.6 2 6 2s2.1 0 2.635.272a2.5 2.5 0 0 1 1.093 1.093C10 3.9 10 4.6 10 6s0 2.1-.272 2.635a2.5 2.5 0 0 1-1.093 1.093C8.1 10 7.4 10 6 10s-2.1 0-2.635-.272a2.5 2.5 0 0 1-1.093-1.093C2 8.1 2 7.4 2 6m0 12c0-1.4 0-2.1.272-2.635a2.5 2.5 0 0 1 1.093-1.092C3.9 14 4.6 14 6 14s2.1 0 2.635.273a2.5 2.5 0 0 1 1.093 1.092C10 15.9 10 16.6 10 18s0 2.1-.272 2.635a2.5 2.5 0 0 1-1.093 1.092C8.1 22 7.4 22 6 22s-2.1 0-2.635-.273a2.5 2.5 0 0 1-1.093-1.092C2 20.1 2 19.4 2 18m12 0c0-1.4 0-2.1.273-2.635a2.5 2.5 0 0 1 1.092-1.092C15.9 14 16.6 14 18 14s2.1 0 2.635.273a2.5 2.5 0 0 1 1.092 1.092C22 15.9 22 16.6 22 18s0 2.1-.273 2.635a2.5 2.5 0 0 1-1.092 1.092C20.1 22 19.4 22 18 22s-2.1 0-2.635-.273a2.5 2.5 0 0 1-1.092-1.092C14 20.1 14 19.4 14 18" color="currentColor"/>
14792    </svg>
14793  </button>
14794<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata">
14795<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M7.5 4.945H16a1.5 1.5 0 0 1 1.5 1.5v1.5m-2.5 5H9m3 4H9"/><path d="M18.497 2H6.307c-.496 0-1.005.073-1.406.368c-1.274.935-2.256 3.02-.273 4.903c.556.528 1.334.72 2.099.72h11.557c.793 0 2.216.113 2.216 2.536v7.454c0 2.22-1.79 4.019-3.997 4.019h-9.03c-2.204 0-3.807-1.557-3.933-3.929L3.506 5.166"/></g></svg>
14796</button>
14797<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;">
14798      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14799        <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11 3l-.663.234c-2.578.91-3.868 1.365-4.602 2.403S5 8.043 5 10.777v2.445c0 2.735 0 4.102.735 5.14c.734 1.039 2.024 1.494 4.602 2.404L11 21m10-9H11m10 0c0-.7-1.994-2.008-2.5-2.5M21 12c0 .7-1.994 2.008-2.5 2.5" color="currentColor"/>
14800      </svg>
14801    </button>
14802    
14802<script>
14803
14804// Initialize mobile menu functionality
14805function initMobileMenu() {
14806  const hamburgerBtn = document.getElementById('hamburgerBtn');
14807  const sidebar = document.querySelector('.sidebar');
14808  const mobileBackdrop = document.getElementById('mobileBackdrop');
14809
14810  // --- ADDED CHECK: Only update header if layout is NOT horizontal ---
14811  let currentLayout = 'sidebar'; // Default
14812  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
14813      currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
14814  }
14815
14816  // --- END ADDED CHECK ---
14817
14818  // Ensure backdrop is properly hidden for horizontal layout on page load
14819  if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) {
14820    mobileBackdrop.classList.remove('mobile-open');
14821    mobileBackdrop.classList.remove('active');
14822    // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open
14823    mobileBackdrop.style.display = '';
14824    mobileBackdrop.style.pointerEvents = '';
14825    mobileBackdrop.style.opacity = '';
14826    mobileBackdrop.style.visibility = '';
14827  }
14828
14829  // Toggle mobile menu when hamburger is clicked
14830  hamburgerBtn.addEventListener('click', function(e) {
14831    e.stopPropagation();
14832    toggleMobileMenu(); // Assumes this function exists
14833  });
14834
14835  // Close mobile menu when backdrop is clicked
14836  if (mobileBackdrop) {
14837    mobileBackdrop.addEventListener('click', function() {
14838      closeMobileMenu(); // Assumes this function exists
14839    });
14840  }
14841
14842  // Escape closes mobile menu (keyboard / SR)
14843  document.addEventListener('keydown', function(e) {
14844    if (e.key !== 'Escape') return;
14845    const sb = document.querySelector('.sidebar');
14846    if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') {
14847      closeMobileMenu();
14848    }
14849  });
14850
14851  // Handle window resize events to ensure proper state
14852  window.addEventListener('resize', function() {
14853    // If we're not on mobile anymore, remove mobile-open class
14854    if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
14855      closeMobileMenu(); // Assumes this function exists
14856    }
14857  });
14858}
14859
14860document.addEventListener('keydown', function(e) {
14861  // Check for caret (Shift+6)
14862  if (e.key === '^' || (e.key === '6' && e.shiftKey)) {
14863    // Only work in edit mode
14864    if (window.isInEditMode && window.isInEditMode()) {
14865      console.log('Import shortcut detected');
14866      toggleImportClassicForm();
14867      e.preventDefault();
14868    }
14869  }
14870});
14871
14872// Enhanced tooltip management with automatic timeouts
14873
14874// Store active tooltip timeouts by ID
14875window.tooltipTimeouts = {};
14876
14877function showTooltip(trigger) {
14878    const tooltipText = trigger.getAttribute('data-custom-tooltip');
14879    if (!tooltipText) return;
14880
14881    // Get or create the main tooltip container
14882    let container = document.getElementById('tooltip-container');
14883    if (!container) {
14884        console.log("Tooltip container not found, creating one.");
14885        container = document.createElement('div'); // Assign to 'container'
14886        container.id = 'tooltip-container';
14887        // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc.
14888        document.body.appendChild(container);
14889    }
14890
14891    const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`;
14892
14893    // Remove any existing tooltip for this trigger
14894    const existingTooltip = document.getElementById(tooltipId);
14895    if (existingTooltip) {
14896        existingTooltip.remove();
14897    }
14898
14899    // Clear any existing timeout for this tooltip
14900    if (!window.tooltipTimeouts) {
14901        window.tooltipTimeouts = {}; // Initialize if it doesn't exist
14902    }
14903    
14904    if (window.tooltipTimeouts[tooltipId]) {
14905        clearTimeout(window.tooltipTimeouts[tooltipId]);
14906        delete window.tooltipTimeouts[tooltipId];
14907    }
14908
14909    // Create tooltip element
14910    const tooltip = document.createElement('div');
14911    tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip
14912    tooltip.id = tooltipId;
14913    tooltip.textContent = tooltipText;
14914
14915    // Add to container (now 'container' is guaranteed to be an element)
14916    container.appendChild(tooltip);
14917
14918    // Position tooltip
14919    if (typeof positionTooltip === 'function') {
14920        positionTooltip(tooltip, trigger);
14921    } else {
14922        console.error("positionTooltip function is not defined.");
14923        tooltip.remove(); // Clean up if we can't position
14924        return;
14925    }
14926
14927    // Show with animation (slight delay for CSS transition)
14928    setTimeout(() => {
14929        tooltip.style.opacity = '1';
14930        // The transform for animation should be handled by CSS transitions
14931        // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); }
14932        // Or ensure positionTooltip sets final transform if it needs to override CSS.
14933        // For now, assuming CSS handles the animated transform from its initial state.
14934    }, 10);
14935
14936    trigger.setAttribute('data-active-tooltip', tooltipId);
14937
14938    // Set automatic timeout to hide after 3 seconds
14939    window.tooltipTimeouts[tooltipId] = setTimeout(() => {
14940        hideTooltip(trigger); // Ensure hideTooltip function exists
14941    }, 3000);
14942    
14943    // Also set up a global timer to ensure all tooltips are dismissed
14944    if (window.globalTooltipTimer) {
14945        clearTimeout(window.globalTooltipTimer);
14946    }
14947    window.globalTooltipTimer = setTimeout(() => {
14948        // Force hide all visible tooltips after 3 seconds
14949        const allTooltips = document.querySelectorAll('.tooltip');
14950        allTooltips.forEach(tooltip => {
14951            if (tooltip.style.opacity !== '0') {
14952                tooltip.style.opacity = '0';
14953                setTimeout(() => {
14954                    if (tooltip.parentNode) {
14955                        tooltip.parentNode.removeChild(tooltip);
14956                    }
14957                }, 200);
14958            }
14959        });
14960        // Clear all timeout references
14961        if (window.tooltipTimeouts) {
14962            for (const id in window.tooltipTimeouts) {
14963                clearTimeout(window.tooltipTimeouts[id]);
14964            }
14965            window.tooltipTimeouts = {};
14966        }
14967    }, 3000);
14968}
14969
14970// Function to hide a tooltip
14971function hideTooltip(trigger) {
14972  const tooltipId = trigger.getAttribute('data-active-tooltip');
14973  if (!tooltipId) return;
14974  
14975  const tooltip = document.getElementById(tooltipId);
14976  if (tooltip) {
14977    // Fade out
14978    tooltip.style.opacity = '0';
14979    tooltip.style.transform = 'translateX(-50%) translateY(0)';
14980    
14981    // Remove after animation
14982    setTimeout(() => {
14983      if (tooltip.parentNode) {
14984        tooltip.parentNode.removeChild(tooltip);
14985      }
14986    }, 200);
14987  }
14988  
14989  // Clear any active timeout for this tooltip
14990  if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) {
14991    clearTimeout(window.tooltipTimeouts[tooltipId]);
14992    delete window.tooltipTimeouts[tooltipId];
14993  }
14994  
14995  // Clear reference
14996  trigger.removeAttribute('data-active-tooltip');
14997  
14998  // If this was the last tooltip, clear the global timer
14999  const remainingTooltips = document.querySelectorAll('.tooltip');
15000  if (remainingTooltips.length === 0 && window.globalTooltipTimer) {
15001    clearTimeout(window.globalTooltipTimer);
15002    window.globalTooltipTimer = null;
15003  }
15004}
15005
15006// Function to initialize tooltips across the document
15007function initTooltipSystem() {
15008  // Check if container already exists
15009  let tooltipContainer = document.getElementById('tooltip-container');
15010  
15011  // Create it if it doesn't exist
15012  if (!tooltipContainer) {
15013    tooltipContainer = document.createElement('div');
15014    tooltipContainer.id = 'tooltip-container';
15015    document.body.appendChild(tooltipContainer);
15016  }
15017  
15018  // Find all elements with data-custom-tooltip attribute
15019  const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]');
15020  
15021  // Add event listeners to each trigger
15022  tooltipTriggers.forEach(trigger => {
15023    // Skip if already initialized
15024    if (trigger.hasAttribute('data-custom-tooltip-initialized')) return;
15025    
15026    // Mark as initialized
15027    trigger.setAttribute('data-custom-tooltip-initialized', 'true');
15028    
15029    // MouseEnter: Show tooltip
15030    trigger.addEventListener('mouseenter', () => {
15031      showTooltip(trigger);
15032    });
15033    
15034    // MouseLeave: Hide tooltip
15035    trigger.addEventListener('mouseleave', () => {
15036      hideTooltip(trigger);
15037    });
15038    
15039    // Click: Hide tooltip immediately
15040    trigger.addEventListener('click', () => {
15041      hideTooltip(trigger);
15042    });
15043    
15044    // Also handle focus/blur for accessibility
15045    trigger.addEventListener('focus', () => {
15046      showTooltip(trigger);
15047    });
15048    
15049    trigger.addEventListener('blur', () => {
15050      hideTooltip(trigger);
15051    });
15052  });
15053  
15054  // Set up a failsafe function to clear any orphaned tooltips
15055  window.clearAllTooltips = function() {
15056    const container = document.getElementById('tooltip-container');
15057    if (container) {
15058      container.innerHTML = '';
15059    }
15060    
15061    // Clear all timeouts
15062    if (window.tooltipTimeouts) {
15063      for (const id in window.tooltipTimeouts) {
15064        clearTimeout(window.tooltipTimeouts[id]);
15065      }
15066      window.tooltipTimeouts = {};
15067    }
15068    
15069    // Clear global timer
15070    if (window.globalTooltipTimer) {
15071      clearTimeout(window.globalTooltipTimer);
15072      window.globalTooltipTimer = null;
15073    }
15074    
15075    // Clear all data-active-tooltip attributes
15076    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15077      el.removeAttribute('data-active-tooltip');
15078    });
15079  };
15080  
15081  // Initialize new tooltips when DOM changes
15082  setupMutationObserver();
15083  
15084  // Add a global function to manually dismiss all tooltips (for testing)
15085  window.dismissAllTooltips = function() {
15086    const allTooltips = document.querySelectorAll('.tooltip');
15087    allTooltips.forEach(tooltip => {
15088      if (tooltip.style.opacity !== '0') {
15089        tooltip.style.opacity = '0';
15090        setTimeout(() => {
15091          if (tooltip.parentNode) {
15092            tooltip.parentNode.removeChild(tooltip);
15093          }
15094        }, 200);
15095      }
15096    });
15097    
15098    // Clear all timeout references
15099    if (window.tooltipTimeouts) {
15100      for (const id in window.tooltipTimeouts) {
15101        clearTimeout(window.tooltipTimeouts[id]);
15102      }
15103      window.tooltipTimeouts = {};
15104    }
15105    
15106    // Clear global timer
15107    if (window.globalTooltipTimer) {
15108      clearTimeout(window.globalTooltipTimer);
15109      window.globalTooltipTimer = null;
15110    }
15111    
15112    // Clear all data-active-tooltip attributes
15113    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15114      el.removeAttribute('data-active-tooltip');
15115    });
15116  };
15117}
15118
15119function positionTooltip(tooltip, trigger) {
15120    const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates
15121
15122    // To accurately get tooltip dimensions, temporarily make it visible but off-screen.
15123    // The #tooltip-container is position:fixed, so the tooltip's absolute positioning
15124    // will be relative to the viewport.
15125    tooltip.style.visibility = 'hidden'; 
15126    tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display
15127    // No need to change position to 'absolute' here if its CSS is already 'position: absolute;'
15128    // within the 'fixed' #tooltip-container.
15129
15130    const tooltipHeight = tooltip.offsetHeight;
15131    const tooltipWidth = tooltip.offsetWidth;
15132
15133    // Restore visibility; opacity transition will handle the fade-in.
15134    tooltip.style.visibility = ''; 
15135    // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding
15136
15137    // Check if tooltip should be positioned to the right (for conflict indicator)
15138    const tooltipPosition = trigger.getAttribute('data-tooltip-position');
15139    if (tooltipPosition === 'right') {
15140        // Position to the right of the trigger
15141        const viewportWidth = window.innerWidth;
15142        const viewportHeight = window.innerHeight;
15143        let left = triggerRect.right + 10; // 10px gap from the right edge of trigger
15144        let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered
15145        
15146        // Recalculate tooltip dimensions after allowing text wrap
15147        tooltip.style.maxWidth = '200px';
15148        tooltip.style.whiteSpace = 'normal';
15149        tooltip.style.wordWrap = 'break-word';
15150        const tooltipWidthAfterWrap = tooltip.offsetWidth;
15151        const tooltipHeightAfterWrap = tooltip.offsetHeight;
15152        
15153        // If tooltip goes off-screen to the right, try positioning to the left of the button
15154        if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15155            left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button
15156            // If it still goes off the left edge, position it at the left edge with padding
15157            if (left < 5) {
15158                left = 5; // Keep it at least 5px from the left edge
15159                // The tooltip will wrap to fit within the available space
15160                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15161            }
15162        } else {
15163            // Tooltip fits on the right, but make sure it doesn't go off-screen
15164            if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15165                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15166            }
15167        }
15168        
15169        // Recalculate after max-width adjustment
15170        const finalTooltipWidth = tooltip.offsetWidth;
15171        const finalTooltipHeight = tooltip.offsetHeight;
15172        
15173        // Ensure tooltip doesn't go off-screen on the right side
15174        if (left + finalTooltipWidth > viewportWidth - 5) {
15175            left = viewportWidth - finalTooltipWidth - 5;
15176        }
15177        
15178        // Adjust if tooltip goes off-screen vertically
15179        if (top < 5) {
15180            top = 5;
15181        } else if (top + finalTooltipHeight > viewportHeight - 5) {
15182            top = viewportHeight - finalTooltipHeight - 5;
15183        }
15184        
15185        tooltip.style.left = `${left}px`;
15186        tooltip.style.top = `${top}px`;
15187        tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning
15188        return;
15189    }
15190
15191    // Calculate desired horizontal center point of the trigger
15192    const triggerCenter = triggerRect.left + triggerRect.width / 2;
15193    
15194    // Calculate tooltip's left position to center it under/over trigger's center
15195    let left = triggerCenter - tooltipWidth / 2;
15196
15197    // Default position: above the trigger
15198    // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset
15199    let top = triggerRect.top - tooltipHeight - 10; 
15200    tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS
15201
15202    // Check if it goes off-screen at the top (add a small buffer, e.g., 5px)
15203    if (top < 5) { 
15204        // Position below the trigger
15205        top = triggerRect.bottom + 10; // 10px gap (arrow height + small space)
15206        tooltip.classList.add('tooltip-bottom');
15207    }
15208
15209    // Boundary checks for left position to prevent going off-screen horizontally
15210    const viewportWidth = window.innerWidth;
15211    if (left < 5) { // 5px buffer from left edge
15212        left = 5; 
15213    } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge
15214        left = viewportWidth - tooltipWidth - 5;
15215    }
15216
15217    tooltip.style.left = `${left}px`;
15218    tooltip.style.top = `${top}px`;
15219
15220    // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering.
15221    // Since JS now calculates the exact 'left' for centering,
15222    // we can clear any such transform that might interfere.
15223    // However, if your CSS animation relies on a transform, adjust accordingly.
15224    // If your CSS only uses opacity for transition, this is fine:
15225    tooltip.style.transform = ''; 
15226}
15227
15228// Watch for new tooltip triggers added to the DOM
15229function setupMutationObserver() {
15230  // Check if MutationObserver is available
15231  if (!window.MutationObserver) return;
15232  
15233  // Create observer instance
15234  const observer = new MutationObserver((mutations) => {
15235    let shouldInit = false;
15236    
15237    // Check if relevant nodes were added
15238    mutations.forEach(mutation => {
15239      if (mutation.type === 'childList') {
15240        mutation.addedNodes.forEach(node => {
15241          // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip
15242          if (node.nodeType === 1) { // ELEMENT_NODE
15243            if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) {
15244              shouldInit = true;
15245            } else if (node.querySelectorAll) {
15246              const tooltips = node.querySelectorAll('[data-custom-tooltip]');
15247              if (tooltips.length) shouldInit = true;
15248            }
15249          }
15250        });
15251      }
15252    });
15253    
15254    // If relevant nodes found, reinitialize tooltips
15255    if (shouldInit) {
15256      initTooltipSystem();
15257    }
15258  });
15259  
15260  // Start observing the entire document with the configured parameters
15261  observer.observe(document.body, { 
15262    childList: true, 
15263    subtree: true 
15264  });
15265}
15266
15267// Initialize when DOM is ready
15268document.addEventListener('DOMContentLoaded', initTooltipSystem);
15269
15270// Also initialize immediately if document already loaded
15271if (document.readyState === 'complete' || document.readyState === 'interactive') {
15272  initTooltipSystem();
15273}
15274
15275// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck
15276setInterval(() => {
15277  // Get all visible tooltips
15278  const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]');
15279  
15280  // If any are found, check how long they've been visible
15281  if (visibleTooltips.length > 0) {
15282    console.log('Found visible tooltips during cleanup check, clearing them');
15283    window.clearAllTooltips();
15284  }
15285}, 5000); // 
15286
15287// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes.
15288var _mobileMenuFocusTrapHandler = null;
15289
15290function getMobileMenuFocusables(sidebar) {
15291  if (!sidebar) return [];
15292  return Array.prototype.filter.call(
15293    sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15293([tabindex="-1"])'),
15294    function (el) {
15295      if (el.getAttribute('aria-hidden') === 'true') return false;
15296      var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
15297      if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
15298      return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement;
15299    }
15300  );
15301}
15302
15303function releaseMobileMenuFocusTrap() {
15304  if (_mobileMenuFocusTrapHandler) {
15305    document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15306    _mobileMenuFocusTrapHandler = null;
15307  }
15308}
15309
15310function installMobileMenuFocusTrap(sidebar) {
15311  releaseMobileMenuFocusTrap();
15312  if (!sidebar) return;
15313  _mobileMenuFocusTrapHandler = function (event) {
15314    if (event.key !== 'Tab') return;
15315    var sb = document.querySelector('.sidebar');
15316    if (!sb || !sb.classList.contains('mobile-open')) return;
15317    var focusables = getMobileMenuFocusables(sb);
15318    var hamburger = document.getElementById('hamburgerBtn');
15319    // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer
15320    var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables;
15321    if (!cycle.length) return;
15322    var first = cycle[0];
15323    var last = cycle[cycle.length - 1];
15324    if (event.shiftKey) {
15325      if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) {
15326        event.preventDefault();
15327        last.focus();
15328      }
15329    } else if (document.activeElement === last) {
15330      event.preventDefault();
15331      first.focus();
15332    }
15333  };
15334  document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15335}
15336
15337// Toggle mobile menu state
15338function toggleMobileMenu() {
15339const sidebar = document.querySelector('.sidebar');
15340const hamburgerBtn = document.getElementById('hamburgerBtn');
15341const mobileBackdrop = document.getElementById('mobileBackdrop');
15342
15343// Check if we're in horizontal layout mode
15344const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15345
15346sidebar.classList.toggle('mobile-open');
15347hamburgerBtn.classList.toggle('active');
15348const isOpen = sidebar.classList.contains('mobile-open');
15349hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
15350
15351if (mobileBackdrop) {
15352  if (isHorizontalLayout) {
15353    // For horizontal layout on mobile, use normal backdrop (tap outside to close)
15354    // Clear any inline overrides so CSS can control visibility
15355    mobileBackdrop.style.display = '';
15356    mobileBackdrop.style.pointerEvents = '';
15357    mobileBackdrop.style.opacity = '';
15358    mobileBackdrop.style.visibility = '';
15359    mobileBackdrop.classList.toggle('mobile-open', isOpen);
15360    setTimeout(() => {
15361      mobileBackdrop.classList.toggle('active', isOpen);
15362    }, 10);
15363  } else {
15364    // For other layouts, use normal backdrop behavior
15365    mobileBackdrop.classList.toggle('mobile-open');
15366    // Slight delay to show opacity transition
15367    setTimeout(() => {
15368      mobileBackdrop.classList.toggle('active', isOpen);
15369    }, 10);
15370  }
15371}
15372
15373// Prevent body scrolling when menu is open
15374document.body.style.overflow = isOpen ? 'hidden' : '';
15375
15376if (isOpen) {
15377  // Trap Tab only — do not steal focus on mouse/touch open (avoids focus rings / scroll jumps)
15378  installMobileMenuFocusTrap(sidebar);
15379} else {
15380  releaseMobileMenuFocusTrap();
15381  // Restore focus only if keyboard focus was inside the drawer
15382  if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15383    try { hamburgerBtn.focus(); } catch (e) {}
15384  }
15385}
15386}
15387
15388// Close the mobile menu
15389function closeMobileMenu() {
15390const sidebar = document.querySelector('.sidebar');
15391const hamburgerBtn = document.getElementById('hamburgerBtn');
15392const mobileBackdrop = document.getElementById('mobileBackdrop');
15393const wasOpen = sidebar && sidebar.classList.contains('mobile-open');
15394
15395// Check if we're in horizontal layout mode
15396const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15397
15398sidebar.classList.remove('mobile-open');
15399hamburgerBtn.classList.remove('active');
15400if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false');
15401
15402if (mobileBackdrop) {
15403  if (isHorizontalLayout) {
15404    // For horizontal layout, clear inline overrides and remove mobile-open
15405    mobileBackdrop.style.display = '';
15406    mobileBackdrop.style.pointerEvents = '';
15407    mobileBackdrop.style.opacity = '';
15408    mobileBackdrop.style.visibility = '';
15409    mobileBackdrop.classList.remove('mobile-open');
15410    mobileBackdrop.classList.remove('active');
15411  } else {
15412    // For other layouts, use normal backdrop behavior
15413    mobileBackdrop.classList.remove('active');
15414    setTimeout(() => {
15415      mobileBackdrop.classList.remove('mobile-open');
15416    }, 300); // Match transition duration
15417  }
15418}
15419
15420// Re-enable body scrolling
15421document.body.style.overflow = '';
15422releaseMobileMenuFocusTrap();
15423// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged
15424if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15425  try { hamburgerBtn.focus(); } catch (e) {}
15426}
15427}
15428
15429function updateMobileHeaderContent() {
15430    const currentLayout = typeof getEffectiveMenuLayout === 'function'
15431        ? getEffectiveMenuLayout()
15432        : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
15433            ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'
15434            : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar'));
15435
15436    const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint
15437
15438    const mobileHeaderContent = document.querySelector('.mobile-header-content');
15439    if (!mobileHeaderContent) {
15440        console.warn("updateMobileHeaderContent: .mobile-header-content element not found.");
15441        return;
15442    }
15443    
15444    // On mobile, center the content unless Space Colony (left-aligned title)
15445    if (isMobileView) {
15446        const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme();
15447        mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15448        // Also ensure logo-menu-container doesn't grow
15449        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
15450        if (logoMenuContainer) {
15451            logoMenuContainer.style.flexGrow = '0';
15452            logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15453        }
15454    }
15455
15456    console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`);
15457
15458    // Clear previous generic mobile content (like centered logo/title)
15459    const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image');
15460    const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title');
15461    if (existingMobileImage) {
15462        // The img is always appended directly (never wrapped in an <a>). Remove it straight.
15463        // Also handle any legacy <a>-wrapped images left from older deployments.
15464        const imageParent = existingMobileImage.parentElement;
15465        if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) {
15466            imageParent.remove();
15467        } else {
15468            existingMobileImage.remove();
15469        }
15470    }
15471    // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach
15472    Array.from(mobileHeaderContent.children).forEach(function(child) {
15473        if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) {
15474            child.remove();
15475        }
15476    });
15477    if (existingMobileText) existingMobileText.remove();
15478    
15479    if (currentLayout === 'horizontal' && !isMobileView) {
15480        // Desktop Horizontal Layout:
15481        // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo.
15482        // The menu items are in .horizontal-menu-container.
15483        // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo).
15484        mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) {
15485            el.remove();
15486        });
15487        console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available.");
15488        if (typeof renderHorizontalMenu === 'function') {
15489            // Wait for sidebar elements to be available before rendering
15490            const waitForSidebarElements = (attempts = 0) => {
15491                const maxAttempts = 10;
15492                if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15493                    console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu.");
15494                    renderHorizontalMenu();
15495                } else if (attempts < maxAttempts) {
15496                    const attemptNum = attempts + 1;
15497                    console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")...");
15498                    setTimeout(() => waitForSidebarElements(attempts + 1), 100);
15499                } else {
15500                    console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway.");
15501                    renderHorizontalMenu();
15502                }
15503            };
15504            waitForSidebarElements();
15505        }
15506        return; 
15507    }
15508    
15509    // For Horizontal layout on mobile, we need to clear the horizontal menu items
15510    // and show only the mobile header logo/title
15511    if (currentLayout === 'horizontal' && isMobileView) {
15512        console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo.");
15513        
15514        // Clear horizontal menu items from mobile header
15515        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15515-container');
15516        if (horizontalMenuContainer) {
15517            console.log("updateMobileHeaderContent: Clearing horizontal menu container");
15518            horizontalMenuContainer.innerHTML = '';
15519        } else {
15520            console.log("updateMobileHeaderContent: No horizontal menu container found to clear");
15521        }
15522        
15523        // Clear horizontal header logo from mobile header
15524        const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo');
15525        if (horizontalHeaderLogo) {
15526            console.log("updateMobileHeaderContent: Clearing horizontal header logo");
15527            horizontalHeaderLogo.innerHTML = '';
15528        } else {
15529            console.log("updateMobileHeaderContent: No horizontal header logo found to clear");
15530        }
15531        
15532        // Also clear any existing horizontal menu items that might be directly in mobile-header-content
15533        const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item');
15534        if (existingHorizontalMenuItems.length > 0) {
15535            console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items');
15536            existingHorizontalMenuItems.forEach(item => item.remove());
15537        }
15538    }
15539    
15540    // For Sidebar, Top, OR Horizontal layout on mobile:
15541    // Populate .mobile-header-content with the first sidebar element (logo/title).
15542    // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout.
15543
15544    console.log("updateMobileHeaderContent: Populating with first sidebar element.");
15545    
15546    // For horizontal layout on mobile, use the header-specific function to get logo/title
15547    // For other layouts, use the general function
15548    let firstElement;
15549    if (currentLayout === 'horizontal' && isMobileView) {
15550        firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically
15551        console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout");
15552    } else {
15553        firstElement = getFirstSidebarElement(); // Gets any first element
15554        console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts");
15555    }
15556
15557    if (firstElement) {
15558        console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement));
15559        
15560        if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) {
15561            console.log('updateMobileHeaderContent: Creating mobile header image');
15562            const img = document.createElement('img');
15563            img.src = firstElement.imageUrl || firstElement.imageData;
15564            img.alt = resolveBrandLogoAlt(firstElement);
15565            img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed
15566
15567            let mobileDisplayNode = img;
15568            if (firstElement.colorizeWithSiteColors &&
15569                window.SidebarManager &&
15570                typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
15571              const buildData = Object.assign({}, firstElement);
15572              if (!buildData.imageWidth) {
15573                buildData.imageWidth = 'auto';
15574              }
15575              mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
15576            }
15577
15578            // Opt-in per-image padding wrapper (no-op when unset)
15579            if (window.SidebarManager &&
15580                typeof window.SidebarManager._wrapWithImagePadding === 'function') {
15581              mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement);
15582            }
15583            
15584            // Handle linking: always append the img directly so existing CSS centering is
15585            // not disrupted by an <a> wrapper (which the many !important positioning rules
15586            // were not written to handle). Navigation is wired via a click handler.
15587            if (firstElement.linkType && firstElement.linkType !== 'none') {
15588                let finalLinkHref = '#';
15589                let isValidLink = false;
15590                const linkTarget = firstElement.linkTarget || '_self';
15591                
15592                if (firstElement.linkType === 'external' && firstElement.linkUrl) {
15593                    finalLinkHref = firstElement.linkUrl;
15594                    isValidLink = true;
15595                } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) {
15596                    const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15597                    const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId));
15598                    
15599                    if (targetPage) {
15600                        const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
15601                        if (isPreviewMode) {
15602                            const pathParts = window.location.pathname.split('/').filter(Boolean);
15603                            const siteGuid = pathParts[0];
15604                            if (siteGuid && targetPage.slug) {
15605                                finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
15606                                isValidLink = true;
15607                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15608                                finalLinkHref = targetPage.url;
15609                                isValidLink = true;
15610                            }
15611                        } else {
15612                            if (targetPage.slug) {
15613                                finalLinkHref = '/' + targetPage.slug;
15614                                isValidLink = true;
15615                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15616                                finalLinkHref = targetPage.url;
15617                                isValidLink = true;
15618                            }
15619                        }
15620                    }
15621                }
15622                
15623                if (isValidLink) {
15624                    mobileDisplayNode.style.cursor = 'pointer';
15625                    const capturedHref = finalLinkHref;
15626                    const capturedTarget = linkTarget;
15627                    const capturedLinkType = firstElement.linkType;
15628                    const capturedPageId = firstElement.linkPageId;
15629                    
15630                    mobileDisplayNode.addEventListener('click', (e) => {
15631                        e.preventDefault();
15632                        if (capturedLinkType === 'internal' && capturedPageId) {
15633                            const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15634                            const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId));
15635                            if (targetPage) {
15636                                if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
15637                                    window.loadPage(targetPage.id);
15638                                } else if (typeof window.loadGallery === 'function') {
15639                                    window.loadGallery(targetPage.id);
15640                                } else {
15641                                    window.location.href = capturedHref;
15642                                }
15643                            } else {
15644                                window.location.href = capturedHref;
15645                            }
15646                        } else {
15647                            if (capturedTarget === '_blank') {
15648                                window.open(capturedHref, '_blank');
15649                            } else {
15650                                window.location.href = capturedHref;
15651                            }
15652                        }
15653                    });
15654                }
15655            }
15656            mobileHeaderContent.appendChild(mobileDisplayNode);
15657        } else if (firstElement.type === 'text' && firstElement.textContent) {
15658            console.log('updateMobileHeaderContent: Creating mobile header text');
15659            const textDiv = document.createElement('div');
15660            textDiv.className = 'mobile-header-text'; // For specific styling
15661            textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML
15662            const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
15663            if (titleEl && !titleEl.classList.contains('site-title-text')) {
15664              titleEl.classList.add('site-title-text');
15665            }
15666            mobileHeaderContent.appendChild(textDiv);
15667        } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) {
15668            console.log('updateMobileHeaderContent: Creating mobile header menu title');
15669            // Fallback for 'menu' type, find the first visible gallery title
15670            const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer);
15671            const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu');
15672            const titleDiv = document.createElement('div');
15673            titleDiv.className = 'mobile-header-menu-title'; // For specific styling
15674            titleDiv.textContent = titleText;
15675            mobileHeaderContent.appendChild(titleDiv);
15676        } else { // Default fallback
15677            console.log('updateMobileHeaderContent: Creating mobile header title (fallback)');
15678            const titleDiv = document.createElement('div');
15679            titleDiv.className = 'mobile-header-title'; // For specific styling
15680            titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu';
15681            mobileHeaderContent.appendChild(titleDiv);
15682        }
15683    } else {
15684        // Fallback if no sidebar elements exist
15685         const defaultTitle = document.createElement('div');
15686         defaultTitle.className = 'mobile-header-title';
15687         defaultTitle.textContent = 'Menu';
15688         mobileHeaderContent.appendChild(defaultTitle);
15689        console.log("updateMobileHeaderContent: No first sidebar element found, showing default title.");
15690    }
15691
15692    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15693      syncContentMarginToHorizontalHeader();
15694    }
15695}
15696
15697// Helper function to get the first element in the sidebar
15698function getFirstSidebarElement() {
15699// Check if window.SidebarManager exists and has elements
15700if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15701// Filter to only visible elements
15702const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer');
15703
15704if (visibleElements.length > 0) {
15705  // Sort by position and return the first one
15706  return visibleElements.sort((a, b) => a.position - b.position)[0];
15707}
15708}
15709
15710// If no sidebar elements, return first gallery as fallback
15711if (galleries.length > 0) {
15712const firstVisibleGallery = galleries.find(g => g.visible !== false);
15713if (firstVisibleGallery) {
15714  return {
15715    type: 'menu',
15716    title: firstVisibleGallery.title
15717  };
15718}
15719}
15720
15721// Default fallback
15722return {
15723type: 'text',
15724title: 'Menu'
15725};
15726}
15727// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE
15728// Just replace the updateMobileTitle function and add our initialization code
15729
15730// Replace the existing updateMobileTitle function with this one
15731function updateMobileTitle() {
15732// Always call our implementation that shows the top sidebar element
15733updateMobileHeaderContent();
15734}
15735
15736// Add window resize listener to handle mobile header content updates
15737window.addEventListener('resize', () => {
15738  // Debounce the resize event to avoid excessive calls
15739  clearTimeout(window.resizeTimeout);
15740  window.resizeTimeout = setTimeout(() => {
15741    if (typeof updateMobileHeaderContent === 'function') {
15742      updateMobileHeaderContent();
15743    }
15744    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
15745      applyHorizontalLogoMenuLayoutStyles();
15746    }
15747    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15748      syncContentMarginToHorizontalHeader();
15749    }
15750  }, 250);
15751});
15752
15753//separate initialization function that doesn't replace the original initMobileMenu
15754function initTopElementDisplay() {
15755console.log("Initializing top element display");
15756
15757// Run immediately
15758updateMobileHeaderContent();
15759
15760if (typeof syncContentMarginToHorizontalHeader === 'function') {
15761  setTimeout(syncContentMarginToHorizontalHeader, 150);
15762}
15763
15764// Also hook into loadGallery to ensure it runs whenever galleries change
15765const originalLoadGallery = window.loadGallery;
15766if (typeof originalLoadGallery === 'function') {
15767window.loadGallery = function(id, event, options) {
15768  // Forward all args (including post-save { forceReload: true }) to the underlying loader
15769  const result = originalLoadGallery(id, event, options);
15770  updateMobileHeaderContent();
15771  return result;
15772};
15773}
15774}
15775
15776
15777// Initialize our functionality after a short delay to ensure SidebarManager is ready
15778document.addEventListener('DOMContentLoaded', function() {
15779// Short delay to ensure SidebarManager is initialized
15780setTimeout(initTopElementDisplay, 100);
15781
15782// Also run a bit later to catch any late-loading elements
15783setTimeout(updateMobileHeaderContent, 500);
15784});
15785
15786// Run immediately if document is already loaded
15787if (document.readyState === 'complete' || document.readyState === 'interactive') {
15788setTimeout(initTopElementDisplay, 100);
15789}
15790
15791// Add a final fallback to ensure gallery visibility on window load
15792window.addEventListener('load', function() {
15793  setTimeout(ensureGalleryVisibility, 2000);
15794});
15795
15796
15797(function() {
15798  /**
15799   * EnhancedGallerySystem - Comprehensive gallery script and state management
15800   */
15801  class EnhancedGallerySystem {
15802    constructor() {
15803      // Core state tracking
15804      this.initialized = false;
15805      this.initPromise = null;
15806      this.isLoading = false;
15807      this.currentGalleryId = null;
15808      this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames)
15809
15810      // Track loaded resources
15811      this.loadedScripts = new Set();
15812      this.loadedStyles = new Set();
15813      
15814      // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues)
15815      const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
15816      this.essentialResources = {
15817        scripts: [
15818          { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' },
15819          { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' }
15820        ],
15821        styles: [
15822          { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }
15823        ]
15824      };
15825      
15826      // Keep track of original initialization functions
15827      this.originalInitFunctions = {};
15828      
15829      // Keep reference to gallery state between changes
15830      this.previousGalleryState = null;
15831      
15832      // Track last initialization time per gallery ID to prevent duplicates
15833      this.lastInitializationTime = new Map();
15834      this._loadInProgress = null;
15835      this._loadingGalleryId = null;
15836    }
15837
15838    /**
15839 * Setup the correct gallery container structure
15840 * @param {HTMLElement} container - The main container element
15841 * @returns {boolean} - Success indicator
15842 */
15843setupCorrectGalleryContainer(container) {
15844  console.log('EnhancedGallerySystem: Setting up correct gallery container structure');
15845  
15846  // Validate container
15847  if (!container) {
15848    console.error('EnhancedGallerySystem: Main gallery container is null');
15849    return false;
15850  }
15851  
15852  try {
15853    // Clear the container
15854    container.innerHTML = '';
15855    
15856    // Create the exact container structure expected by the gallery script
15857    const neonContainer = document.createElement('div');
15858    neonContainer.id = 'neon-gallery-container';
15859    neonContainer.style.width = '100%';
15860    neonContainer.style.height = '100%';
15861    neonContainer.style.position = 'relative';
15862    
15863    // Add the container to the DOM
15864    container.appendChild(neonContainer);
15865    
15866    // Set visibility
15867    container.style.opacity = '1';
15868    container.style.display = 'block';
15869    
15870    console.log('EnhancedGallerySystem: Gallery container structure created');
15871    return true;
15872  } catch (error) {
15873    console.error('EnhancedGallerySystem: Error setting up gallery container:', error);
15874    return false;
15875  }
15876}
15877
15878    /**
15879     * Ensure #neon-gallery-container exists before gallery scripts execute.
15880     * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}.
15881     */
15882    ensureGalleryContainerPlaceholder(container) {
15883      if (document.getElementById('neon-gallery-container')) {
15884        return document.getElementById('neon-gallery-container');
15885      }
15886      const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement;
15887      if (!host) {
15888        return null;
15889      }
15890      const neonContainer = document.createElement('div');
15891      neonContainer.id = 'neon-gallery-container';
15892      neonContainer.className = 'gallery-direct-content';
15893      neonContainer.style.width = '100%';
15894      neonContainer.style.height = '100%';
15895      neonContainer.style.position = 'relative';
15896      host.appendChild(neonContainer);
15897      console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load');
15898      return neonContainer;
15899    }
15900
15901    /**
15902     * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed.
15903     */
15904    async ensureNeonGalleryUtilsReady(container) {
15905      const hasLoadGallerySettings = () =>
15906        typeof window.neonGalleryUtils?.loadGallerySettings === 'function';
15907
15908      if (hasLoadGallerySettings()) {
15909        return true;
15910      }
15911
15912      const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main');
15913      if (!mainScript) {
15914        return false;
15915      }
15916
15917      console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing — reloading main script');
15918      this.ensureGalleryContainerPlaceholder(container);
15919
15920      document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
15921        if (el.parentNode) el.parentNode.removeChild(el);
15922      });
15923      this.loadedScripts.delete('neon-gallery-main');
15924      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
15925
15926      await new Promise((resolve, reject) => {
15927        const scriptElement = document.createElement('script');
15928        scriptElement.id = 'neon-gallery-main';
15929        scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`;
15930        scriptElement.async = false;
15931        scriptElement.onload = () => {
15932          this.loadedScripts.add('neon-gallery-main');
15933          resolve();
15934        };
15935        scriptElement.onerror = (err) => reject(err);
15936        document.body.appendChild(scriptElement);
15937      });
15938
15939      for (let i = 0; i < 50; i++) {
15940        if (hasLoadGallerySettings()) {
15941          return true;
15942        }
15943        await new Promise(r => setTimeout(r, 100));
15944      }
15945
15946      return hasLoadGallerySettings();
15947    }
15948
15949    /**
15950     * Preload styles only — defer script loading until a gallery load provides a container.
15951     */
15952    preloadStylesOnly() {
15953      return this.loadStyles();
15954    }
15955
15956    /**
15957     * Reset initialization tracking when gallery scripts are removed from the DOM
15958     * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and
15959     * subsequent loadGallery calls hang with isLoading:true, initialized:false.
15960     */
15961    resetInitializationState() {
15962      console.log('EnhancedGallerySystem: Resetting initialization state');
15963      this.initPromise = null;
15964      this.initialized = false;
15965      this.isLoading = false;
15966      this.loadedScripts.clear();
15967      this.loadedStyles.clear();
15968      // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected
15969      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
15970    }
15971
15972    /**
15973     * Initialize the gallery system
15974     * @returns {Promise} Resolves when all core dependencies are loaded
15975     */
15976    initialize() {
15977      // Return existing promise if already initializing or complete
15978      if (this.initPromise) {
15979        return this.initPromise;
15980      }
15981
15982      console.log('EnhancedGallerySystem: Initializing gallery framework');
15983      this.isLoading = true;
15984
15985      // Create a promise to track loading completion
15986      this.initPromise = (async () => {
15987        try {
15988          // CRITICAL: Initialize neonGalleryState before anything else
15989          // This prevents "Cannot set properties of undefined" errors
15990          if (!window.neonGalleryState) {
15991            window.neonGalleryState = {
15992              initialized: false,
15993              initializing: false,
15994              domInitialized: false,
15995              configApplied: false,
15996              initializationPromise: null
15997            };
15998            console.log('EnhancedGallerySystem: Initialized neonGalleryState');
15999          }
16000
16001          // 1. First ensure all essential styles are loaded
16002          await this.loadStyles();
16003
16004          // 2. Preload all scripts to hint to the browser
16005          this.preloadScripts();
16006
16007          // 3. Ensure gallery container exists before scripts that require it
16008          this.ensureGalleryContainerPlaceholder(
16009            document.querySelector('.gallery-container')
16010          );
16011
16012          // 4. Load all essential scripts
16013          await this.loadScripts();
16014          
16015          // 5. Initialize the gallery events
16016          this.setupEvents();
16017          
16018          // Mark as fully initialized
16019          this.initialized = true;
16020          this.isLoading = false;
16021          
16022          console.log('EnhancedGallerySystem: Successfully initialized');
16023          return true;
16024        } catch (error) {
16025          console.error('EnhancedGallerySystem: Initialization failed:', error);
16026          this.isLoading = false;
16027          throw error;
16028        }
16029      })();
16030
16031      return this.initPromise;
16032    }
16033
16034    /**
16035     * Load all necessary stylesheet resources
16036     */
16037    async loadStyles() {
16038      console.log('EnhancedGallerySystem: Loading essential stylesheets');
16039      
16040      const stylePromises = this.essentialResources.styles.map(style => {
16041        // Skip if already loaded
16042        if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) {
16043          console.log(`Style already loaded: ${style.id}`);
16044          this.loadedStyles.add(style.id);
16045          return Promise.resolve();
16046        }
16047        
16048        return new Promise((resolve, reject) => {
16049          const link = document.createElement('link');
16050          link.id = style.id;
16051          link.rel = 'stylesheet';
16052          // Use proxy URL if flag is set, otherwise use CDN URL
16053          let styleUrl = `${style.href}?v=${this.version}`;
16054          if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) {
16055            styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version;
16056            console.log('🔍 Using proxy URL for style:', style.id, styleUrl);
16057          }
16058          
16059          link.href = styleUrl;
16060          
16061          link.onload = () => {
16062            console.log(`✅ Style loaded: ${style.id}`);
16063            this.loadedStyles.add(style.id);
16064            resolve();
16065          };
16066          
16067          link.onerror = (err) => {
16068            console.error(`❌ Failed to load style: ${style.id}`, err);
16069            if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) {
16070              console.error('🔄 Retrying via proxy:', style.href);
16071              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16072              link.href = proxyUrl + '?v=' + this.version;
16073              link.onerror = function() {
16074                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16075                link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
16076                // Don't reject - let it try storage
16077              };
16078            } else {
16079              reject(new Error(`Failed to load ${style.id}`));
16080            }
16081          };
16082          
16083          document.head.appendChild(link);
16084        });
16085      });
16086      
16087      // Wait for all styles to load
16088      await Promise.all(stylePromises);
16089      console.log('EnhancedGallerySystem: All stylesheets loaded');
16090    }
16091    
16092    /**
16093     * Preload scripts to improve browser loading priority
16094     */
16095    preloadScripts() {
16096      this.essentialResources.scripts.forEach(script => {
16097        // Enhanced check for already preloaded scripts
16098        const scriptName = script.src.split('/').pop();
16099        const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) ||
16100                                document.querySelector(`script[src*="${scriptName}"]`);
16101        
16102        if (alreadyPreloaded) {
16103          console.log(`Script already preloaded or loaded: ${script.id}`);
16104          return;
16105        }
16106        
16107        const preload = document.createElement('link');
16108        preload.rel = 'preload';
16109        preload.href = `${script.src}?v=${this.version}`;
16110        preload.as = 'script';
16111        document.head.appendChild(preload);
16112        
16113        console.log(`Script preloaded: ${script.id}`);
16114      });
16115    }
16116    
16117    /**
16118     * Load all necessary scripts
16119     */
16120    async loadScripts() {
16121      console.log('EnhancedGallerySystem: Loading essential scripts');
16122      
16123      // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils
16124      // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes.
16125      const loadOne = (script) => {
16126        const alreadyLoaded = this.loadedScripts.has(script.id) || 
16127                             document.getElementById(script.id) ||
16128                             document.querySelector(`script[src*="${script.src.split('/').pop()}"]`);
16129        
16130        if (alreadyLoaded) {
16131          const mainNeedsReload =
16132            script.id === 'neon-gallery-main' &&
16133            typeof window.neonGalleryUtils?.loadGallerySettings !== 'function';
16134          if (mainNeedsReload) {
16135            console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete — reloading`);
16136            document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16137              if (el.parentNode) el.parentNode.removeChild(el);
16138            });
16139            this.loadedScripts.delete(script.id);
16140            try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16141          } else {
16142          console.log(`Script already loaded: ${script.id}`);
16143          this.loadedScripts.add(script.id);
16144          return Promise.resolve();
16145          }
16146        }
16147        
16148        return new Promise((resolve, reject) => {
16149          const scriptElement = document.createElement('script');
16150          scriptElement.id = script.id;
16151          
16152          let scriptUrl = `${script.src}?v=${this.version}`;
16153          if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) {
16154            scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version;
16155            console.log('🔍 Using proxy URL for script:', script.id, scriptUrl);
16156          }
16157          
16158          scriptElement.src = scriptUrl;
16159          scriptElement.async = false;
16160          
16161          scriptElement.onload = () => {
16162            console.log(`✅ Script loaded: ${script.id}`);
16163            this.loadedScripts.add(script.id);
16164            resolve();
16165          };
16166          
16167          scriptElement.onerror = (err) => {
16168            console.error(`❌ Failed to load script: ${script.id}`, err);
16169            if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) {
16170              console.error('🔄 Retrying script via proxy:', script.id);
16171              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16172              scriptElement.src = proxyUrl + '?v=' + this.version;
16173              scriptElement.onerror = function() {
16174                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16175                scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
16176              };
16177            } else {
16178              reject(new Error(`Failed to load ${script.id}`));
16179            }
16180          };
16181          
16182          document.body.appendChild(scriptElement);
16183        });
16184      };
16185      
16186      for (const script of this.essentialResources.scripts) {
16187        await loadOne(script);
16188      }
16189      console.log('EnhancedGallerySystem: All scripts loaded');
16190      
16191      // Save references to original initialization functions after scripts are loaded
16192      if (window.initNeonGallery && typeof window.initNeonGallery === 'function') {
16193        this.originalInitFunctions.initNeonGallery = window.initNeonGallery;
16194      }
16195      
16196      if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') {
16197        this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave;
16198      }
16199    }
16200
16201    /**
16202     * Set up enhanced event listeners
16203     */
16204    setupEvents() {
16205      // Listen for gallery save events to capture latest state
16206      document.addEventListener('gallery-saved', (event) => {
16207        if (event.detail && event.detail.settings) {
16208          this.captureGalleryState(event.detail.settings);
16209        }
16210      });
16211      
16212      // Override existing functions to capture state changes
16213      this.enhanceExistingFunctions();
16214    }
16215    
16216    /**
16217     * Enhance existing gallery functions to work with our system
16218     */
16219    enhanceExistingFunctions() {
16220      // Save original functions
16221      const originalInit = window.initNeonGallery;
16222      const originalReinit = window.reinitializeGalleryAfterSave;
16223      
16224      // Only enhance if they exist
16225      if (typeof originalInit === 'function') {
16226        // Track if initNeonGallery is currently executing to prevent concurrent calls
16227        let isExecuting = false;
16228        let lastExecutionTime = 0;
16229        
16230        window.initNeonGallery = async (...args) => {
16231          // Prevent duplicate initializations within 1 second
16232          const now = Date.now();
16233          
16234          // Check if already executing or called recently
16235          if (isExecuting) {
16236            console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call');
16237            return Promise.resolve();
16238          }
16239          
16240          if (lastExecutionTime && (now - lastExecutionTime) < 1000) {
16241            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)');
16242            return Promise.resolve();
16243          }
16244          
16245          // Set flag and timestamp immediately to prevent concurrent calls
16246          isExecuting = true;
16247          lastExecutionTime = now;
16248          this.lastInitializationTime.set('initNeonGallery', now);
16249          
16250          try {
16251            // Call original with all arguments
16252            const result = await originalInit.apply(this, args);
16253            
16254            // Capture state after initialization
16255            if (window.neonGalleryConfig) {
16256              this.captureGalleryState(window.neonGalleryConfig);
16257            }
16258            
16259            return result;
16260          } finally {
16261            // Clear execution flag after a short delay to allow for async operations
16262            setTimeout(() => {
16263              isExecuting = false;
16264            }, 100);
16265          }
16266        };
16267      }
16268      
16269      if (typeof originalReinit === 'function') {
16270        window.reinitializeGalleryAfterSave = async (savedSettings) => {
16271          // Call original function
16272          const result = await originalReinit(savedSettings);
16273          
16274          // Capture updated state
16275          this.captureGalleryState(savedSettings);
16276          
16277          return result;
16278        };
16279      }
16280    }
16281    
16282    /**
16283     * Capture current gallery state for potential reuse
16284     */
16285    captureGalleryState(settings) {
16286      if (!settings) return;
16287      
16288      // Store a deep copy of the settings to avoid reference issues
16289      try {
16290        this.previousGalleryState = JSON.parse(JSON.stringify(settings));
16291        console.log('EnhancedGallerySystem: Captured gallery state for possible reuse');
16292      } catch (e) {
16293        console.error('EnhancedGallerySystem: Failed to capture gallery state:', e);
16294      }
16295    }
16296
16297    /**
16298     * Load a specific gallery using the enhanced system
16299     * @param {Object} gallery The gallery configuration
16300     * @param {HTMLElement} container The container to load the gallery into
16301     */
16302    async loadGallery(gallery, container, options) {
16303      if (!gallery) {
16304        console.error('EnhancedGallerySystem: Invalid gallery configuration');
16305        return false;
16306      }
16307
16308      const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
16309
16310      // Coalesce concurrent loads for the same gallery — await instead of wiping mid-flight
16311      if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) {
16312        const pageViewStillShowing = window._activeContentType === 'page' ||
16313          !!document.querySelector('.gallery-container .page-container, .page-container');
16314        if (!pageViewStillShowing) {
16315          console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id);
16316          return this._loadInProgress;
16317        }
16318        console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id);
16319        this._loadInProgress = null;
16320        this._loadingGalleryId = null;
16321      }
16322
16323      const now = Date.now();
16324      const lastInitTime = this.lastInitializationTime.get(gallery.id);
16325      if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) {
16326        const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry');
16327        const pageViewStillShowing = window._activeContentType === 'page' ||
16328          !!document.querySelector('.gallery-container .page-container, .page-container');
16329        if (hasContent && !pageViewStillShowing) {
16330          console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init');
16331          if (container) container.style.opacity = '1';
16332          if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
16333            window.syncGalleryOptionsButtonVisibility();
16334          }
16335          return true;
16336        }
16337      }
16338
16339      if (forceGalleryReload) {
16340        console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id);
16341        this.lastInitializationTime.delete(gallery.id);
16342        this._loadInProgress = null;
16343        this._loadingGalleryId = null;
16344      }
16345
16346      const loadTask = this._executeGalleryLoad(gallery, container, now, options);
16347      this._loadInProgress = loadTask;
16348      this._loadingGalleryId = gallery.id;
16349      try {
16350        return await loadTask;
16351      } finally {
16352        if (this._loadInProgress === loadTask) {
16353          this._loadInProgress = null;
16354          this._loadingGalleryId = null;
16355        }
16356      }
16357    }
16358
16359    async _executeGalleryLoad(gallery, container, startedAt, options) {
16360      this.lastInitializationTime.set(gallery.id, startedAt);
16361      const contentLoadGeneration = options && options.contentLoadGeneration != null
16362        ? options.contentLoadGeneration
16363        : window.__hydraContentLoadGeneration;
16364      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16365        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16366        : contentLoadGeneration === window.__hydraContentLoadGeneration &&
16367          (!window._activeContentType || window._activeContentType === 'gallery') &&
16368          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16369
16370      stopAutoAdvanceTimer();
16371
16372      // CRITICAL: Verify container exists
16373      if (!container) {
16374        console.error('EnhancedGallerySystem: Gallery container is null or undefined');
16375        return false;
16376      }
16377
16378  // Create a unique instance ID for this gallery load (needed before script init)
16379  const galleryInstanceId = Date.now();
16380
16381  try {
16382    // Clean up previous gallery state and prepare container BEFORE loading scripts
16383    this.cleanupGalleryState(container);
16384    const containerSetup = this.setupCorrectGalleryContainer(container);
16385    if (!containerSetup) {
16386      throw new Error('Failed to set up gallery container');
16387    }
16388    this.setupGalleryParameters(gallery, galleryInstanceId);
16389  } catch (prepError) {
16390    console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError);
16391    return false;
16392  }
16393
16394  // Make sure the system is initialized (scripts load with container already in DOM)
16395  try {
16396    await this.initialize();
16397  } catch (error) {
16398    console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error);
16399    //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.');
16400    return false;
16401  }
16402
16403  if (!stillCurrent()) {
16404    console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id);
16405    return false;
16406  }
16407
16408  if (!(await this.ensureNeonGalleryUtilsReady(container))) {
16409    console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load');
16410    return false;
16411  }
16412
16413  if (!stillCurrent()) {
16414    console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id);
16415    return false;
16416  }
16417
16418  // Update current gallery tracking
16419  this.currentGalleryId = gallery.id;
16420  
16421  console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`);
16422
16423  try {
16424    // Determine which initialization method to use
16425    if (this.canUseReinitForGallery(gallery, options)) {
16426      const result = await this.reinitializeGallery(gallery, container, options);
16427      return stillCurrent() ? result : false;
16428    } else {
16429      const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16430      return stillCurrent() ? result : false;
16431    }
16432  } catch (error) {
16433    console.error('EnhancedGallerySystem: Error loading gallery:', error);
16434    if (container) {
16435      console.error('EnhancedGallerySystem: Error loading gallery:', error);
16436      //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.');
16437    }
16438    return false;
16439  }
16440}
16441    
16442    normalizeGalleryIdentityValue(value) {
16443      if (value === undefined || value === null || value === '') return '';
16444      return String(value);
16445    }
16446
16447    previousGalleryStateBelongsTo(gallery, previousState) {
16448      if (!gallery || !previousState) return false;
16449
16450      const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id);
16451      const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId);
16452      if (!targetGalleryId && !targetPageId) return false;
16453
16454      const previousGalleryId = this.normalizeGalleryIdentityValue(
16455        previousState.galleryId || previousState.loadedGalleryId
16456      );
16457      const previousPageId = this.normalizeGalleryIdentityValue(
16458        previousState.pageId || previousState.initialPageUuid
16459      );
16460
16461      if (!previousGalleryId && !previousPageId) return false;
16462      if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) {
16463        return false;
16464      }
16465      if (previousPageId && targetPageId && previousPageId !== targetPageId) {
16466        return false;
16467      }
16468
16469      return (
16470        (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) ||
16471        (previousPageId && targetPageId && previousPageId === targetPageId)
16472      );
16473    }
16474
16475    /**
16476     * Determine if we can use reinitialization for this gallery
16477     */
16478    canUseReinitForGallery(gallery, options) {
16479      if (options?.forceReload === true || window._forceGalleryReload === true) {
16480        return false;
16481      }
16482      // Check if reinitializeGalleryAfterSave function exists
16483      if (typeof window.reinitializeGalleryAfterSave !== 'function') {
16484        return false;
16485      }
16486      
16487      // Check if we have a previous state to reuse
16488      if (!this.previousGalleryState) {
16489        return false;
16490      }
16491
16492      if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) {
16493        console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery');
16494        return false;
16495      }
16496      
16497      // Check if the gallery has the necessary properties for reinit
16498      if (!gallery.pageId || !gallery.galleryOptions) {
16499        return false;
16500      }
16501      
16502      // CRITICAL: Check if the gallery wrapper actually exists in the DOM
16503      // Reinitialization requires an existing gallery structure
16504      const galleryWrapper = document.querySelector('.gallery-wrapper');
16505      const neonContainer = document.getElementById('neon-gallery-container');
16506      if (!galleryWrapper || !neonContainer) {
16507        console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found');
16508        return false;
16509      }
16510      
16511      return true;
16512    }
16513    
16514    /**
16515     * Reinitialize a gallery using the reinitialization process
16516     */
16517async reinitializeGallery(gallery, container, options) {
16518  console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id);
16519  
16520  try {
16521    // CRITICAL: Ensure CSS is loaded before attempting reinitialization
16522    ensureGalleryStylesLoaded();
16523    
16524    // We should already have the proper container, but just in case:
16525    if (!document.getElementById('neon-gallery-container')) {
16526      this.setupCorrectGalleryContainer(container);
16527    }
16528    
16529    // CRITICAL: Ensure config is set up with all required fields before reinitialization
16530    const galleryOptions = gallery.galleryOptions || {};
16531    window.neonGalleryConfig = {
16532      useData: true,
16533      useCDN: true,
16534      version: 'live',
16535      manualCollectionName: galleryOptions.manualCollectionName || "mod",
16536      layoutType: galleryOptions.layoutType || "grid",
16537      ...galleryOptions,
16538      siteId: window.siteId || gallery.siteId || '',
16539      galleryId: gallery.id,
16540      // Explicitly include all permanent storage fields
16541      isPerma: galleryOptions.isPerma,
16542      permaURL: galleryOptions.permaURL,
16543      loadTxId: galleryOptions.loadTxId,
16544      loadPermaURL: galleryOptions.loadPermaURL,
16545      containerSelector: '#neon-gallery-container'
16546    };
16547    
16548    // Prepare settings by merging previous state with new gallery options.
16549    // Identity fields always come from the menu target, never leftover snapshot ids.
16550    const settings = {
16551      ...this.previousGalleryState,
16552      ...gallery.galleryOptions,
16553      pageId: gallery.pageId,
16554      initialPageUuid: gallery.pageId,
16555      galleryId: gallery.id,
16556      loadedGalleryId: gallery.id,
16557      forceReload: true,
16558      containerSelector: '#neon-gallery-container' // Add this explicitly
16559    };
16560    
16561    // Call reinitializeGalleryAfterSave with the merged settings
16562    window._reinitializeGalleryFromSystem = true;
16563    let result;
16564    try {
16565      result = await window.reinitializeGalleryAfterSave(settings);
16566    } finally {
16567      window._reinitializeGalleryFromSystem = false;
16568    }
16569    
16570    if (result) {
16571      console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully');
16572      // Fade in the container
16573      container.style.opacity = '1';
16574      return true;
16575    } else {
16576      throw new Error('Reinitialization did not complete successfully');
16577    }
16578  } catch (error) {
16579    console.error('EnhancedGallerySystem: Error reinitializing gallery:', error);
16580    // Fall back to standard initialization - ensure config is set up properly
16581    const galleryInstanceId = Date.now();
16582    // Re-setup parameters and config for the fallback
16583    this.setupGalleryParameters(gallery, galleryInstanceId);
16584    return this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16585  }
16586}
16587    
16588    /**
16589     * Initialize a gallery from scratch (fallback method)
16590     */
16591    async initializeNewGallery(gallery, container, galleryInstanceId, options) {
16592      console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id);
16593      const contentLoadGeneration = options && options.contentLoadGeneration != null
16594        ? options.contentLoadGeneration
16595        : window.__hydraContentLoadGeneration;
16596      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16597        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16598        : (!window._activeContentType || window._activeContentType === 'gallery') &&
16599          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16600      
16601      try {
16602        // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements
16603        ensureGalleryStylesLoaded();
16604        
16605        // Clean up container
16606        if (container) {
16607          container.innerHTML = '';
16608        } else {
16609          console.error('EnhancedGallerySystem: Container is null in initializeNewGallery');
16610          return false;
16611        }
16612        
16613        // Create gallery container with unique ID
16614        const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`;
16615        const neonGalleryContainer = document.createElement('div');
16616        neonGalleryContainer.id = 'neon-gallery-container';
16617        neonGalleryContainer.className = 'gallery-direct-content';
16618        neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
16619        neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString());
16620        neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId);
16621        neonGalleryContainer.style.width = '100%';
16622        neonGalleryContainer.style.height = '100%';
16623        container.appendChild(neonGalleryContainer);
16624        
16625        // CRITICAL: Initialize neonGalleryState if it doesn't exist
16626        if (!window.neonGalleryState) {
16627          window.neonGalleryState = {
16628            initialized: false,
16629            initializing: false,
16630            domInitialized: false,
16631            configApplied: false,
16632            initializationPromise: null
16633          };
16634        }
16635
16636        // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields
16637        // Don't rely on conditional logic - always set it correctly here
16638        const galleryOptions = gallery.galleryOptions || {};
16639        window.neonGalleryConfig = {
16640          useData: true,
16641          useCDN: true,
16642          version: 'live',
16643          manualCollectionName: galleryOptions.manualCollectionName || "mod",
16644          layoutType: galleryOptions.layoutType || "grid",
16645          ...galleryOptions,
16646          siteId: window.siteId || gallery.siteId || '',
16647          galleryId: gallery.id,
16648          galleryInstanceId: galleryInstanceId,
16649          containerSelector: '#neon-gallery-container',
16650          // Explicitly include all permanent storage fields (critical for proper initialization)
16651          isPerma: galleryOptions.isPerma,
16652          permaURL: galleryOptions.permaURL,
16653          loadTxId: galleryOptions.loadTxId,
16654          loadPermaURL: galleryOptions.loadPermaURL
16655        };
16656        
16657        // Call the initialization function directly if available
16658        if (typeof window.initNeonGallery === 'function') {
16659          console.log('EnhancedGallerySystem: Calling direct initialization method');
16660          
16661          // Additional check: prevent duplicate initNeonGallery calls within 1 second
16662          const initKey = 'init_' + gallery.id + '_' + galleryInstanceId;
16663          const lastInitCall = this.lastInitializationTime.get(initKey);
16664          const now = Date.now();
16665          if (lastInitCall && (now - lastInitCall) < 1000) {
16666            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + ' 
16666(last call ' + (now - lastInitCall) + 'ms ago)');
16667            container.style.opacity = '1';
16668            return true;
16669          }
16670          this.lastInitializationTime.set(initKey, now);
16671          
16672          try {
16673            // Wait a tiny bit to ensure DOM updates have propagated
16674            await new Promise(resolve => setTimeout(resolve, 10));
16675            if (!stillCurrent()) {
16676              console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery');
16677              return false;
16678            }
16679            
16680            const result = await window.initNeonGallery();
16681            if (!stillCurrent()) {
16682              console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion');
16683              return false;
16684            }
16685            console.log('EnhancedGallerySystem: Direct initialization successful');
16686            
16687            // Save state for future loads
16688            if (window.neonGalleryConfig) {
16689              this.captureGalleryState(window.neonGalleryConfig);
16690            }
16691            
16692            // Fade in the container
16693            container.style.opacity = '1';
16694            return true;
16695          } catch (e) {
16696            if (!stillCurrent()) {
16697              console.log('EnhancedGallerySystem: Ignoring stale direct initialization error');
16698              return false;
16699            }
16700            console.error('EnhancedGallerySystem: Direct initialization failed:', e);
16701            throw e;
16702          }
16703        } else {
16704          // If initNeonGallery isn't available, use event-based initialization
16705          console.log('EnhancedGallerySystem: Using event-based initialization fallback');
16706          
16707          // Create an event to trigger initialization
16708          const initEvent = new CustomEvent('neon-gallery-init', { 
16709            detail: { 
16710              galleryId: gallery.id,
16711              instanceId: galleryInstanceId,
16712              containerId: galleryContainerId
16713            }
16714          });
16715          document.dispatchEvent(initEvent);
16716          
16717          // Wait a short time to see if initialization completes
16718          await new Promise(resolve => setTimeout(resolve, 500));
16719          if (!stillCurrent()) {
16720            console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization');
16721            return false;
16722          }
16723          
16724          // Fade in regardless of result - gallery script should handle displaying errors
16725          container.style.opacity = '1';
16726          return true;
16727        }
16728      } catch (error) {
16729        console.error('EnhancedGallerySystem: Error in standard initialization:', error);
16730        if (!stillCurrent()) {
16731          console.log('EnhancedGallerySystem: Ignoring stale standard initialization error');
16732          return false;
16733        }
16734        if (container) {
16735          this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.');
16736        }
16737        return false;
16738      }
16739    }
16740
16741    /**
16742     * Clean up gallery state without removing loaded scripts
16743     */
16744    cleanupGalleryState(container) {
16745      console.log('EnhancedGallerySystem: Cleaning up gallery state');
16746      
16747      // Stop any ongoing singles autoplay from the previous gallery
16748      if (typeof window.stopSinglesAutoplay === 'function') {
16749        try {
16750          window.stopSinglesAutoplay(); 
16751          console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.');
16752        } catch (error) {
16753          console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error);
16754        }
16755      } else {
16756        console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.');
16757      }
16758
16759      // 1. Reset global gallery state variables
16760      const resetGlobals = [
16761        'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
16762        'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 
16763        'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 
16764        'neonGalleryInstance', 'currentGalleryId', 'galleryData', 
16765        'imageCache', 'thumbnailCache'
16766      ];
16767      
16768      resetGlobals.forEach(prop => {
16769        if (window[prop] !== undefined) {
16770          window[prop] = null;
16771        }
16772      });
16773      
16774      // 2. Reset neonGalleryState if it's a proper object with properties
16775      if (window.neonGalleryState && typeof window.neonGalleryState === 'object') {
16776        window.neonGalleryState = {
16777          initialized: false,
16778          initializing: false,
16779          domInitialized: false,
16780          configApplied: false,
16781          initializationPromise: null
16782        };
16783      }
16784      
16785      // 3. Remove lightbox elements and overlays
16786      document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => {
16787        if (el && el.parentNode) {
16788          el.parentNode.removeChild(el);
16789        }
16790      });
16791
16792      const staleOptionsOverlay = document.getElementById('optionsOverlay');
16793      if (staleOptionsOverlay && staleOptionsOverlay.parentNode) {
16794        staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay);
16795      }
16796      window.optionsVisible = false;
16797      
16798      // 4. Remove any gallery-specific DOM elements
16799      document.querySelectorAll(
16800        '.neon-gallery-wrapper, .fullscreen-overlay, ' + 
16801        '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' +
16802        '.neon-thumbnails, .neon-image, .neon-caption, ' +
16803        '.neon-controls, .neon-pagination'
16804      ).forEach(el => {
16805        if (el && el.parentNode) {
16806          el.parentNode.removeChild(el);
16807        }
16808      });
16809      
16810      // 5. Clear container if provided and valid
16811      if (container) {
16812        container.innerHTML = '';
16813        container.style.opacity = '0';
16814      }
16815      
16816      // 6. Clear keyboard listeners to prevent multiple listeners from different galleries
16817      if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
16818        window.removeAllLightboxKeyboardListeners();
16819        console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup');
16820      } else if (window.currentLightboxKeyboardListener) {
16821        document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
16822        window.currentLightboxKeyboardListener = null;
16823        console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup');
16824      }
16825      window.lightboxKeyboardListenerActive = false;
16826    }
16827    
16828    /**
16829     * Prepare container for gallery loading
16830     */
16831    prepareContainer(container, gallery, instanceId) {
16832      // CRITICAL: Add null check for container
16833      if (!container) {
16834        console.error('EnhancedGallerySystem: Container is null in prepareContainer');
16835        return;
16836      }
16837      
16838      try {
16839        // Add attributes and data
16840        container.setAttribute('data-gallery-id', gallery.id.toString());
16841        container.setAttribute('data-instance-id', instanceId.toString());
16842        
16843        // Add loading indicator
16844        const loadingIndicator = document.createElement('div');
16845        loadingIndicator.className = 'gallery-loading';
16846        loadingIndicator.innerHTML = `
16847          <div class="spinner"></div>
16848          <div class="loading-text">Loading Gallery...</div>
16849        `;
16850        
16851        container.appendChild(loadingIndicator);
16852      } catch (error) {
16853        console.error('EnhancedGallerySystem: Error preparing container:', error);
16854      }
16855    }
16856    
16857
16858
16859
16860setupGalleryParameters(gallery, galleryInstanceId) {
16861  // Set up gallery parameters with enhanced uniqueness
16862  window.Parameters = window.Parameters || {};
16863  
16864  // Clone and extend existing Parameters to avoid losing important values
16865  const existingParams = { ...window.Parameters };
16866  
16867  window.Parameters = {
16868    ...existingParams,
16869    SiteAlias: window.location.hostname,
16870    InitialPageUuid: gallery.pageId,
16871    InitialPageAlias: gallery.slug || this.slugify(gallery.title),
16872    // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode
16873    // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active
16874    isInEditor: window.isEditing || 
16875                localStorage.getItem('hydra_is_admin') === 'true' || 
16876                window.isAdmin === true || 
16877                document.body.classList.contains('hydra-admin'),
16878    siteId: window.siteId || gallery.siteId || '',
16879    isHydra: true,
16880    
16881    // CRITICAL: These properties help the gallery script find the container
16882    galleryContainerId: 'neon-gallery-container',
16883    containerSelector: '#neon-gallery-container',
16884    
16885    // Additional properties
16886    galleryInstanceId: galleryInstanceId,
16887    galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`,
16888    loadTimestamp: galleryInstanceId,
16889    loadedGalleryId: gallery.id
16890  };
16891
16892  // Set up gallery config - CRITICAL: Include all permanent storage fields
16893  const galleryOptions = gallery.galleryOptions || {};
16894  window.neonGalleryConfig = {
16895    useData: true,
16896    useCDN: true,
16897    version: 'live',
16898    manualCollectionName: galleryOptions.manualCollectionName || "mod",
16899    layoutType: galleryOptions.layoutType || "grid",
16900    ...galleryOptions,
16901    siteId: window.siteId || gallery.siteId || '',
16902    galleryId: gallery.id,
16903    galleryInstanceId: galleryInstanceId,
16904    
16905    // Add container selector here too
16906    containerSelector: '#neon-gallery-container',
16907    
16908    // CRITICAL: Explicitly include all permanent storage fields to ensure they're available
16909    // These are needed for proper gallery initialization and data loading
16910    isPerma: galleryOptions.isPerma,
16911    permaURL: galleryOptions.permaURL,
16912    loadTxId: galleryOptions.loadTxId,
16913    loadPermaURL: galleryOptions.loadPermaURL
16914  };
16915}
16916    
16917    /**
16918     * Show error message in container
16919     */
16920    showErrorMessage(container, message) {
16921      // CRITICAL: Add null check for container
16922      if (!container) {
16923        console.error('EnhancedGallerySystem: Container is null in showErrorMessage');
16924        return;
16925      }
16926      
16927      try {
16928        // Clear container
16929        container.innerHTML = '';
16930        
16931        // Create error message element
16932        const errorElement = document.createElement('div');
16933        errorElement.className = 'gallery-error';
16934        errorElement.innerHTML = `
16935          <h3>Error Loading Gallery</h3>
16936          <p>${message}</p>
16937        `;
16938        container.appendChild(errorElement);
16939        container.style.opacity = '1';
16940      } catch (error) {
16941        console.error('EnhancedGallerySystem: Error showing error message:', error);
16942      }
16943    }
16944    
16945    /**
16946     * Utility function to convert title to slug
16947     */
16948    slugify(text) {
16949      return text
16950        .toLowerCase()
16951        .replace(/s+/g, '-')
16952        .replace(/[^w-]+/g, '')
16953        .replace(/--+/g, '-')
16954        .trim();
16955    }
16956  }
16957
16958  // Create a singleton instance
16959  window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem();
16960
16961  /**
16962   * Enhanced loadGallery function using the EnhancedGallerySystem
16963   * This replaces the existing loadGallery function
16964   */
16965const enhancedLoadGallery = async function(id, event, options) {
16966    const originalLoadGallery = window.originalLoadGallery;
16967    const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
16968
16969    stopAutoAdvanceTimer();
16970    if (typeof dismissGalleryPasswordOverlay === 'function') {
16971      dismissGalleryPasswordOverlay();
16972    }
16973
16974    if (event && event.target && event.target.isConnected === false) {
16975      console.log('enhancedLoadGallery: Ignoring navigation from detached element');
16976      return;
16977    }
16978
16979    if (event) {
16980      event.preventDefault();
16981      event.stopPropagation();
16982    }
16983
16984    let galleryContainer = document.querySelector('.gallery-container');
16985
16986    if (!galleryContainer) {
16987      galleryContainer = document.getElementById('galleryFrame')?.parentElement ||
16988                        document.getElementById('main-content') ||
16989                        document.getElementById('content');
16990
16991      if (!galleryContainer) {
16992        console.error('Gallery container not found, falling back to original function');
16993        if (typeof originalLoadGallery === 'function') {
16994          return originalLoadGallery(id, event, options);
16995        } else {
16996          console.error('Gallery container not found and no fallback available');
16997          return;
16998        }
16999      }
17000    }
17001
17002    // Bail out before hiding/clearing when the same gallery is already loading or rendered.
17003    // Post-save passes { forceReload: true } to bypass this path without affecting navigation.
17004    const egEarly = window.EnhancedGallerySystem;
17005    if (egEarly && !forceGalleryReload) {
17006      if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) {
17007        const pageViewStillShowing = window._activeContentType === 'page' ||
17008          !!document.querySelector('.gallery-container .page-container, .page-container');
17009        if (pageViewStillShowing) {
17010          console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id);
17011          egEarly._loadInProgress = null;
17012          egEarly._loadingGalleryId = null;
17013        } else {
17014          console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id);
17015          await egEarly._loadInProgress;
17016          if (galleryContainer) galleryContainer.style.opacity = '1';
17017          return;
17018        }
17019      }
17020      const hasRenderedGallery = !!document.querySelector(
17021        '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'
17022      );
17023      // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view.
17024      const pageViewStillShowing = window._activeContentType === 'page' ||
17025        !!document.querySelector('.gallery-container .page-container, .page-container');
17026      if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) {
17027        console.log('enhancedLoadGallery: gallery already rendered, skipping reload');
17028        if (galleryContainer) galleryContainer.style.opacity = '1';
17029        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17030          window.syncGalleryOptionsButtonVisibility();
17031        }
17032        return;
17033      }
17034    } else if (forceGalleryReload) {
17035      console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip');
17036      if (egEarly?.lastInitializationTime?.delete) {
17037        egEarly.lastInitializationTime.delete(id);
17038      }
17039      if (egEarly) {
17040        egEarly._loadInProgress = null;
17041        egEarly._loadingGalleryId = null;
17042      }
17043    }
17044
17045    if (galleryContainer) {
17046      galleryContainer.style.opacity = '0';
17047    }
17048
17049    console.log('Loading gallery with ID (enhancedLoadGallery):', id);
17050
17051    const gallery = findGalleryById(galleries, id);
17052
17053    if (!gallery) {
17054      console.warn('No gallery found with ID:', id);
17055      if (galleryContainer) {
17056        galleryContainer.innerHTML = `
17057          <div class="gallery-error">
17058            <h3>Gallery Not Found</h3>
17059            <p>The requested gallery could not be found.</p>
17060          </div>
17061        `;
17062        galleryContainer.style.opacity = '1';
17063      }
17064      return;
17065    }
17066
17067    if (isPageMenuItem(gallery)) {
17068      return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event);
17069    }
17070
17071    window._activeContentType = 'gallery';
17072
17073    if (gallery.isSubmenu) {
17074      console.log('Gallery is submenu, skipping URL update');
17075      toggleSubmenu(id, event || { stopPropagation: () => {} });
17076      if (galleryContainer){
17077      galleryContainer.style.opacity = '1';
17078      }
17079      return;
17080    }
17081
17082    const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
17083    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
17084      window.PageManager.clearCurrentPage();
17085    }
17086
17087    console.log('Found gallery:', gallery.title);
17088
17089    // Check for gallery-level password protection (only for visitors, not in edit mode)
17090    if (!isInEditMode() && gallery.passwordProtected) {
17091      // Use PageManager's gallery password protection methods
17092      if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 
17093          typeof window.PageManager.createGalleryPasswordOverlay === 'function') {
17094        
17095        if (!window.PageManager.checkGalleryPasswordAccess(gallery)) {
17096          console.log('Gallery is password protected, showing overlay');
17097          window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => {
17098            if (accessGranted) {
17099              // Reload the gallery now that access is granted
17100              enhancedLoadGallery(id, event);
17101            }
17102          });
17103          return; // Exit early, will reload after password is entered
17104        }
17105      }
17106    }
17107
17108    window.activeGalleryId = id;
17109    activeGalleryId = id;
17110
17111    // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery.
17112    // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(),
17113    // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original
17114    // loadGallery) and clear the container so page content does not block gallery rendering.
17115    if (typeof window.removeGalleryScriptsWithPause === 'function') {
17116      await window.removeGalleryScriptsWithPause();
17117    }
17118    if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17119      console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id);
17120      return;
17121    }
17122    if (galleryContainer) {
17123      galleryContainer.innerHTML = '';
17124    }
17125
17126    const bodyEl = document.body;
17127    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
17128        bodyEl.classList.add('menu-hidden-on-page');
17129    } else {
17130        bodyEl.classList.remove('menu-hidden-on-page');
17131    }
17132    applyMenuOverlayClassForGallery(gallery);
17133
17134    try {
17135      if (typeof updateActiveStates === 'function') updateActiveStates();
17136      if (typeof updateMobileTitle === 'function') updateMobileTitle();
17137      if (typeof closeMobileMenu === 'function') closeMobileMenu();
17138      // Close the gallery options panel when navigating to a different gallery
17139      if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) {
17140        window.closeOptionsPanel();
17141      }
17142      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17143      if (staleOptionsOverlay) {
17144        staleOptionsOverlay.remove();
17145      }
17146      window.optionsVisible = false;
17147      if (typeof updateURLWithGallerySlug === 'function') {
17148        updateURLWithGallerySlug(gallery);
17149      }
17150    } catch (uiError) {
17151      console.error('Error updating UI state:', uiError);
17152    }
17153
17154    try {
17155      // Add detailed logging about EnhancedGallerySystem availability
17156      console.log('🔍 Checking EnhancedGallerySystem availability:', {
17157        exists: !!window.EnhancedGallerySystem,
17158        hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17159        initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized),
17160        isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading)
17161      });
17162      
17163      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') {
17164        console.log('✅ EnhancedGallerySystem available, calling loadGallery');
17165        const result = await window.EnhancedGallerySystem.loadGallery(
17166          gallery,
17167          galleryContainer,
17168          {
17169            ...(options || {}),
17170            ...(forceGalleryReload ? { forceReload: true } : {}),
17171            contentLoadGeneration
17172          }
17173        );
17174        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17175          console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id);
17176          return;
17177        }
17178        console.log('📊 EnhancedGallerySystem.loadGallery result:', result);
17179
17180        if (!result && typeof originalLoadGallery === 'function') {
17181          console.log('⚠️ Enhanced gallery loading failed, falling back to original function');
17182          return originalLoadGallery(id, event, options);
17183        }
17184        if (galleryContainer) {
17185          galleryContainer.style.opacity = '1';
17186        }
17187        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17188          window.syncGalleryOptionsButtonVisibility();
17189          setTimeout(window.syncGalleryOptionsButtonVisibility, 350);
17190        }
17191      } else {
17192        console.error('❌ EnhancedGallerySystem or its loadGallery method is not available.', {
17193          EnhancedGallerySystem: !!window.EnhancedGallerySystem,
17194          loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17195          useCdnProxy: window.useCdnProxy || false
17196        });
17197        
17198        // Fallback: Try using createGalleryContext directly
17199        console.log('🔄 Attempting fallback: createGalleryContext');
17200        if (typeof createGalleryContext === 'function') {
17201          createGalleryContext(gallery, galleryContainer);
17202          if (galleryContainer) {
17203            galleryContainer.style.opacity = '1';
17204          }
17205          return;
17206        }
17207        
17208        if (galleryContainer) {
17209            galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`;
17210            galleryContainer.style.opacity = '1';
17211        }
17212        return;
17213      }
17214    } catch (error) {
17215      console.error('Error loading gallery:', error);
17216      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17217        console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id);
17218        return;
17219      }
17220      if (galleryContainer) {
17221        try {
17222          galleryContainer.innerHTML = `
17223            <div class="gallery-error">
17224              <h3>Error Loading Gallery</h3>
17225              <p>There was a problem loading the gallery. Please try again later.</p>
17226              <p class="error-details">${error.message}</p>
17227            </div>
17228          `;
17229          galleryContainer.style.opacity = '1';
17230        } catch (containerError) {
17231          console.error('Error showing error message in container:', containerError);
17232        }
17233      }
17234      if (typeof originalLoadGallery === 'function') {
17235        console.log('Falling back to original loadGallery after error');
17236        return originalLoadGallery(id, event, options);
17237      }
17238    }
17239  };
17240
17241  /**
17242   * Initialization function to set up the gallery system
17243   */
17244  function initializeGallerySystem() {
17245    console.log('Initializing enhanced gallery system');
17246    
17247    // Store reference to original function before replacing
17248    if (typeof window.loadGallery === 'function') {
17249      window.originalLoadGallery = window.loadGallery;
17250    }
17251    
17252    // Replace the existing loadGallery function with our optimized version
17253    window.loadGallery = enhancedLoadGallery;
17254    
17255    // Add this CSS for loading/error states
17256    addGallerySystemStyles();
17257    
17258    // Preload styles only at boot — scripts load when first gallery opens (with container ready)
17259    window.EnhancedGallerySystem.preloadStylesOnly().catch(error => {
17260      console.warn('Gallery styles preloading failed:', error);
17261    });
17262  }
17263
17264  /**
17265   * Add CSS styles for loading indicators and error messages
17266   */
17267  function addGallerySystemStyles() {
17268    const styleId = 'enhanced-gallery-system-styles';
17269    
17270    // Skip if already added
17271    if (document.getElementById(styleId)) {
17272      return;
17273    }
17274    
17275    const style = document.createElement('style');
17276    style.id = styleId;
17277    style.textContent = `
17278      /* Loading indicator */
17279      .gallery-loading {
17280        display: flex;
17281        flex-direction: column;
17282        justify-content: center;
17283        align-items: center;
17284        width: 100%;
17285        height: 100%;
17286        min-height: 200px;
17287        opacity: 0 !important;
17288      }
17289      
17290      .gallery-loading .spinner {
17291        width: 50px;
17292        height: 50px;
17293        border: 3px solid rgba(0, 0, 0, 0.1);
17294        border-radius: 50%;
17295        border-top-color: #4682B4;
17296        animation: spin 1s ease-in-out infinite;
17297        margin-bottom: 20px;
17298        opacity: 0 !important;
17299      }
17300      
17301      @keyframes spin {
17302        to { transform: rotate(360deg); }
17303      }
17304      
17305      .gallery-loading .loading-text {
17306        font-family: Arial, sans-serif;
17307        font-size: 16px;
17308        color: #555;
17309        opacity: 0;
17310      }
17311      
17312      /* Error message */
17313      .gallery-error {
17314        display: flex;
17315        flex-direction: column;
17316        justify-content: center;
17317        align-items: center;
17318        width: 100%;
17319        height: 100%;
17320        min-height: 200px;
17321        padding: 30px;
17322        text-align: center;
17323      }
17324      
17325      .gallery-error h3 {
17326        font-family: Arial, sans-serif;
17327        font-size: 24px;
17328        color:rgba(211, 47, 47, 0.25);
17329        margin-bottom: 20px;
17330      }
17331      
17332      .gallery-error p {
17333        font-family: Arial, sans-serif;
17334        font-size: 16px;
17335        color: #555;
17336        max-width: 600px;
17337        margin-bottom: 10px;
17338      }
17339      
17340      .gallery-error .error-details {
17341        font-size: 14px;
17342        color: #888;
17343        font-style: italic;
17344      }
17345    `;
17346    
17347    document.head.appendChild(style);
17348  }
17349
17350  // Initialize gallery system immediately if possible
17351  initializeGallerySystem();
17352})();
17353
17354
17355/**
17356 * Enhanced Gallery Link Interceptor
17357 * Handles clicks on gallery links to prevent full page reloads
17358 */
17359(function() {
17360  // Store any loaded gallery data for quick reference
17361  const GALLERY_DATA_CACHE = {
17362    initialized: false,
17363    galleries: [],
17364    lastUpdate: 0
17365  };
17366
17367  // Initialize when DOM is ready to catch early clicks
17368  document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor);
17369  
17370  // Also initialize when window loads to ensure complete gallery data
17371  window.addEventListener('load', function() {
17372    // Use a small delay to ensure galleries have fully initialized
17373    setTimeout(initGalleryLinkInterceptor, 200);
17374  });
17375  
17376  // Additional init on first user interaction
17377  document.addEventListener('click', function initOnFirstClick() {
17378    initGalleryLinkInterceptor();
17379    // Remove this listener after first click
17380    document.removeEventListener('click', initOnFirstClick);
17381  }, { once: true });
17382  
17383  // Main initialization function
17384  function initGalleryLinkInterceptor() {
17385    // Prevent multiple initializations
17386    if (window._galleryLinkInterceptorInitialized) {
17387      // If already initialized, just refresh the gallery data
17388      refreshGalleryCache();
17389      return;
17390    }
17391    
17392    window._galleryLinkInterceptorInitialized = true;
17393    console.log('Initializing enhanced gallery link interceptor');
17394    
17395    // Update our cache with current galleries data
17396    refreshGalleryCache();
17397    
17398    // Get primary containers that might contain links
17399    const galleryContainer = document.querySelector('.gallery-container');
17400    const mainContainer = document.querySelector('.container') || document.body;
17401    
17402    if (!galleryContainer && !mainContainer) {
17403      console.warn('No gallery containers found, link interceptor partially initialized');
17404    }
17405    
17406    // Add global click handler to catch all link clicks
17407    document.addEventListener('click', handleGlobalLinkClick, true);
17408    
17409    // Try to intercept links from iframes - critical for gallery content
17410    interceptAllIframeLinks();
17411    
17412    // Set up a mutation observer to handle dynamically added content
17413    setupMutationObserver();
17414    
17415    // Log initialization with site details
17416    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17417    console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`);
17418    
17419    // Create global helper for debugging
17420    window.debugGalleryLinks = function() {
17421      return {
17422        inPreviewMode: isPreviewMode,
17423        interceptorInitialized: window._galleryLinkInterceptorInitialized,
17424        cachedGalleries: GALLERY_DATA_CACHE.galleries.length,
17425        urlPath: window.location.pathname,
17426        siteGuid: getSiteGuidFromPath()
17427      };
17428    };
17429  }
17430  
17431  // Update the gallery cache when gallery data changes
17432function refreshGalleryCache() {
17433  // Initialize GALLERY_DATA_CACHE if it doesn't exist
17434  if (!window.GALLERY_DATA_CACHE) {
17435    window.GALLERY_DATA_CACHE = {
17436      initialized: false,
17437      galleries: [],
17438      lastUpdate: 0
17439    };
17440  }
17441  
17442  // Check for global galleries variable first
17443  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
17444    // Use the global galleries array
17445    window.GALLERY_DATA_CACHE.galleries = galleries;
17446    window.GALLERY_DATA_CACHE.initialized = true;
17447    window.GALLERY_DATA_CACHE.lastUpdate = Date.now();
17448    window.galleries = galleries;
17449
17450    console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`);
17451    return;
17452  }
17453    
17454    // Debug log
17455    console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`);
17456  }
17457  
17458  // Global click handler - catches all link clicks
17459  function handleGlobalLinkClick(e) {
17460    // Find if a link was clicked
17461    const link = e.target.closest('a');
17462    if (!link || !link.href) return;
17463    
17464    // Skip if the link uses special targets or modifiers
17465    if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17466    
17467    // Skip for hash links or javascript: links
17468    if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17469    
17470    try {
17471      // Parse the URL to work with it
17472      const url = new URL(link.href);
17473      
17474      // Only intercept links to the same domain
17475      if (url.hostname !== window.location.hostname) return;
17476      
17477      // Skip API and resource URLs
17478      if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return;
17479      
17480      console.log('Link intercepted:', url.pathname);
17481      
17482      // For preview URLs, we need to handle the path specially
17483      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17484      const siteGuid = getSiteGuidFromPath();
17485      
17486      console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid);
17487
17488      // Site Graph card links carry data-sg-target-id — use SPA navigation (not preview iframe fallback)
17489      if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) {
17490        let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries))
17491          ? galleries
17492          : window.galleries;
17493        const sgItem = Array.isArray(galleriesData)
17494          ? findGalleryById(galleriesData, link.dataset.sgTargetId)
17495          : null;
17496        if (sgItem) {
17497          e.preventDefault();
17498          e.stopPropagation();
17499          if (sgItem.isPage && window.loadPage) {
17500            window.loadPage(sgItem.id);
17501          } else if (window.loadGallery) {
17502            window.loadGallery(sgItem.id);
17503          }
17504          let targetUrl = url.href;
17505          if (isPreviewMode && siteGuid) {
17506            const pathname = url.pathname.startsWith(`/${siteGuid}/`) ?
17507              url.pathname :
17508              `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17509            const newUrl = new URL(url);
17510            newUrl.pathname = pathname;
17511            targetUrl = newUrl.href;
17512          }
17513          window.history.pushState(
17514            { galleryId: sgItem.id },
17515            sgItem.title || document.title,
17516            targetUrl
17517          );
17518          document.title = formatGalleryDocumentTitle(sgItem.title);
17519          window.scrollTo(0, 0);
17520          return;
17521        }
17522      }
17523      
17524      // Check if this is internal navigation first
17525      let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17526      
17527      console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none');
17528      
17529      if (matchingItem) {
17530        // Prevent default navigation (full page reload)
17531        e.preventDefault();
17532        e.stopPropagation();
17533        
17534        console.log('Found matching gallery for link:', matchingItem.title);
17535
17536        const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function'
17537          ? window.isNestedGalleryLinkModeEnabled()
17538          : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes);
17539        const isGridGalleryLink = !!(link.closest('.masonry-item') ||
17540          (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') &&
17541           link.querySelector('img') &&
17542           !link.closest('#neon-lightbox, .neon-lightbox')));
17543
17544        if (!matchingItem.isPage &&
17545            nestedLinkModeActive &&
17546            isGridGalleryLink &&
17547            typeof window.openNestedGalleryLightbox === 'function') {
17548          void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => {
17549            if (!opened) {
17550              console.warn('Nested gallery lightbox failed — staying on parent gallery (no loadGallery fallback)');
17551            }
17552          });
17553          return;
17554        }
17555        
17556        // Use the existing navigation functions
17557        if (matchingItem.isPage && window.loadPage) {
17558          window.loadPage(matchingItem.id);
17559        } else if (window.loadGallery) {
17560          window.loadGallery(matchingItem.id);
17561        }
17562        
17563        // Construct proper URL for preview mode
17564        let targetUrl = url.href;
17565        if (isPreviewMode && siteGuid) {
17566          // Ensure we keep the GUID in the URL path
17567          const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17568            url.pathname : 
17569            `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17570          
17571          const newUrl = new URL(url);
17572          newUrl.pathname = pathname;
17573          targetUrl = newUrl.href;
17574        }
17575        
17576        // Update URL without reload
17577        window.history.pushState(
17578          { galleryId: matchingItem.id },
17579          matchingItem.title || document.title,
17580          targetUrl
17581        );
17582        
17583        // Update page title
17584        document.title = formatGalleryDocumentTitle(matchingItem.title);
17585        
17586        // Scroll to top
17587        window.scrollTo(0, 0);
17588        
17589        return;
17590      } 
17591      // Special case for preview mode - even if no matching gallery found
17592      else if (isPreviewMode && siteGuid) {
17593        // We're in preview mode with a GUID - intercept the navigation
17594        // to maintain the GUID in the URL. Only do this when #galleryFrame
17595        // can load content; otherwise allow SPA onclick handlers to run.
17596        
17597        console.log('No direct match but in preview mode - handling navigation');
17598
17599        const frame = document.getElementById('galleryFrame');
17600
17601        if (!frame) {
17602          console.warn('Gallery frame not found - allowing default/SPA handlers');
17603        } else {
17604          // Prevent default navigation
17605          e.preventDefault();
17606          e.stopPropagation();
17607          
17608          // Construct a URL that preserves the GUID
17609          let newPathname = url.pathname;
17610          if (!newPathname.startsWith(`/${siteGuid}/`)) {
17611            // Add the GUID to the path if not already there
17612            newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17613          }
17614          
17615          // Create the preview-friendly URL for browser history
17616          const browserUrl = new URL(window.location.origin);
17617          browserUrl.pathname = newPathname;
17618          
17619          console.log('Constructing preview URL with GUID:', browserUrl.href);
17620          
17621          // IMPORTANT: For gallery navigation, we need to create a special URL
17622          // that tells the gallery system to load the content directly
17623          console.log('Loading preview path in iframe:', newPathname);
17624          
17625          // First update browser URL to show the correct path
17626          window.history.pushState(
17627            { customPath: newPathname },
17628            document.title,
17629            browserUrl.href
17630          );
17631          
17632          // Create direct path to the gallery content
17633          const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
17634          
17635          // For direct gallery paths, we need to load the gallery with special parameters
17636          // This tells the gallery system to load content from this path
17637          const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`;
17638          
17639          // Two approaches to loading the content:
17640          
17641          // 1. Use the gallery wrapper approach
17642          const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
17643            `?siteId=${siteGuid}` +
17644            `&path=${encodeURIComponent(cleanPath)}` + 
17645            `&directPath=true` +
17646            `&t=${Date.now()}`; // Cache buster
17647          
17648          console.log('Loading gallery via wrapper URL:', wrapperUrl);
17649          frame.src = wrapperUrl;
17650          
17651          // Clear any active gallery ID since we're navigating directly
17652          if (window.activeGalleryId) {
17653            console.log('Clearing active gallery ID during direct navigation');
17654            window.activeGalleryId = null;
17655          }
17656          
17657          return; // Critical: prevent further navigation
17658        }
17659      }
17660      
17661      console.log('No matching gallery or special handling - allow normal navigation');
17662    } catch (error) {
17663      console.error('Error processing link click:', error);
17664    }
17665  }
17666  
17667  // Handle all iframes on the page
17668  function interceptAllIframeLinks() {
17669    // Look for any iframe that might contain gallery content
17670    const iframes = document.querySelectorAll('iframe');
17671    
17672    iframes.forEach(iframe => {
17673      if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) {
17674        // Primary gallery iframe - high priority
17675        setupIframeIntercept(iframe, true);
17676      } else {
17677        // Any other iframe - still try to intercept
17678        setupIframeIntercept(iframe, false);
17679      }
17680    });
17681    
17682    console.log(`Attempted to intercept links in ${iframes.length} iframes`);
17683  }
17684  
17685  // Setup link interception for a specific iframe
17686  function setupIframeIntercept(iframe, isPrimary) {
17687    try {
17688      // Skip if iframe is not fully loaded yet
17689      if (!iframe.contentWindow || !iframe.contentDocument) {
17690        // Add load event to try again when loaded
17691        iframe.addEventListener('load', function() {
17692          setupIframeIntercept(iframe, isPrimary);
17693        });
17694        return;
17695      }
17696      
17697      // Try to access the iframe content - might fail due to same-origin policy
17698      try {
17699        const iframeDoc = iframe.contentDocument;
17700        const iframeWin = iframe.contentWindow;
17701        
17702        // Add event listener for all link clicks in the iframe
17703        iframeDoc.addEventListener('click', function(e) {
17704          const link = e.target.closest('a');
17705          if (!link || !link.href) return;
17706          
17707          // Skip special cases
17708          if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17709          if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17710          
17711          try {
17712            const url = new URL(link.href);
17713            
17714            // Check if same domain or relative URL
17715            const isSameDomain = url.hostname === window.location.hostname || 
17716                                url.hostname === iframe.src.hostname || 
17717                                !url.hostname;
17718            
17719            if (isSameDomain) {
17720              const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17721              const siteGuid = getSiteGuidFromPath();
17722              
17723              // First try to find a matching gallery/page
17724              const matchingItem = parent.findGalleryByPath ? 
17725                parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 
17726                findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17727              
17728              if (matchingItem) {
17729                // We found a match - prevent default and navigate
17730                e.preventDefault();
17731                e.stopPropagation();
17732                
17733                console.log('Iframe: Found matching content for link:', matchingItem.title);
17734                
17735                // Access parent window functions
17736                if (matchingItem.isPage && parent.loadPage) {
17737                  parent.loadPage(matchingItem.id);
17738                } else if (parent.loadGallery) {
17739                  parent.loadGallery(matchingItem.id);
17740                }
17741                
17742                // Update URL in parent window
17743                let targetUrl = url.href;
17744                if (isPreviewMode && siteGuid) {
17745                  // Ensure GUID is preserved in URL
17746                  const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17747                    url.pathname : 
17748                    `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17749                  
17750                  const newUrl = new URL(window.location.origin + pathname);
17751                  targetUrl = newUrl.href;
17752                }
17753                
17754                // Update browser URL
17755                parent.history.pushState(
17756                  { galleryId: matchingItem.id },
17757                  matchingItem.title || document.title,
17758                  targetUrl
17759                );
17760                
17761                // Update parent window title
17762                parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent);
17763                
17764                // Scroll parent to top
17765                parent.scrollTo(0, 0);
17766                return;
17767              } 
17768              // Special handling for preview mode - critical fix here!
17769              else if (isPreviewMode && siteGuid) {
17770                // Special handling for preview URLs
17771                e.preventDefault();
17772                e.stopPropagation();
17773                
17774                console.log('Iframe: No direct match but in preview mode - handling navigation');
17775                
17776                // Construct a URL that preserves the GUID
17777                let newPathname = url.pathname;
17778                if (!newPathname.startsWith(`/${siteGuid}/`)) {
17779                  newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17780                }
17781                
17782                // Create a URL for the browser history
17783                const browserUrl = new URL(window.location.origin);
17784                browserUrl.pathname = newPathname;
17785                
17786                console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href);
17787                
17788                // Update browser URL in parent
17789                parent.history.pushState(
17790                  { customPath: newPathname },
17791                  document.title,
17792                  browserUrl.href
17793                );
17794                
17795                // Handle the navigation in the parent window
17796                if (isPrimary && parent.document.getElementById('galleryFrame')) {
17797                  const galleryFrame = parent.document.getElementById('galleryFrame');
17798                  
17799                  // Create clean path for the gallery system
17800                  const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
17801                  
17802                  // Create wrapper URL for gallery content
17803                  const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
17804                    `?siteId=${siteGuid}` +
17805                    `&path=${encodeURIComponent(cleanPath)}` + 
17806                    `&directPath=true` +
17807                    `&t=${Date.now()}`; // Cache buster
17808                  
17809                  console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl);
17810                  galleryFrame.src = wrapperUrl;
17811                  
17812                  // Clear any active gallery ID since we're navigating directly
17813                  if (parent.activeGalleryId) {
17814                    console.log('Iframe: Clearing active gallery ID during direct navigation');
17815                    parent.activeGalleryId = null;
17816                  }
17817                } else {
17818                  // Fallback - navigate normally in case we can't handle it
17819                  console.log('Iframe: Gallery frame not found or not primary, navigating normally');
17820                  window.location.href = browserUrl.href;
17821                }
17822                return;
17823              }
17824            }
17825          } catch (iframeError) {
17826            console.error('Error handling iframe link:', iframeError);
17827          }
17828        });
17829        
17830        if (isPrimary) {
17831          console.log('Successfully intercepted links in primary gallery iframe');
17832        }
17833      } catch (sameOriginError) {
17834        // This is expected for cross-origin iframes
17835        if (isPrimary) {
17836          console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.');
17837          
17838          // Apply a fallback method - add postMessage listener
17839          setupPostMessageListener(iframe);
17840        }
17841      }
17842    } catch (error) {
17843      console.warn('Error setting up iframe link interception:', error);
17844    }
17845  }
17846  
17847  // Fallback: use postMessage for cross-origin iframes
17848  function setupPostMessageListener(iframe) {
17849    // Listen for messages from the iframe
17850    window.addEventListener('message', function(event) {
17851      try {
17852        // Verify the message is from our iframe
17853        if (event.source !== iframe.contentWindow) return;
17854        
17855        const data = event.data;
17856        
17857        // Check if this is a link click event
17858        if (data && data.type === 'linkClick' && data.href) {
17859          console.log('Received link click message from iframe:', data.href);
17860          
17861          // Create a URL object to work with the link
17862          const url = new URL(data.href);
17863          
17864          // Check if it's a local navigation
17865          if (url.hostname === window.location.hostname) {
17866            const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17867            const siteGuid = getSiteGuidFromPath();
17868            
17869            // Try to find a matching gallery
17870            const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17871            
17872            if (matchingItem) {
17873              console.log('Found matching gallery for postMessage link:', matchingItem.title);
17874              
17875              // Navigate using the appropriate function
17876              if (matchingItem.isPage && window.loadPage) {
17877                window.loadPage(matchingItem.id);
17878              } else if (window.loadGallery) {
17879                window.loadGallery(matchingItem.id);
17880              }
17881              
17882              // Update URL without reload
17883              let targetUrl = url.href;
17884              if (isPreviewMode && siteGuid) {
17885                // Ensure GUID is preserved
17886                const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17887                  url.pathname : 
17888                  `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17889                
17890                const newUrl = new URL(window.location.origin + pathname);
17891                targetUrl = newUrl.href;
17892              }
17893              
17894              window.history.pushState(
17895                { galleryId: matchingItem.id },
17896                matchingItem.title || document.title,
17897                targetUrl
17898              );
17899              
17900              // Update page title
17901              document.title = formatGalleryDocumentTitle(matchingItem.title);
17902              
17903              return;
17904            } 
17905            // Special handling for preview mode - Fixed!
17906            else if (isPreviewMode && siteGuid) {
17907              console.log('No direct match but in preview mode - handling navigation via postMessage');
17908              
17909              // Construct URL that preserves the GUID
17910              let newPathname = url.pathname;
17911              if (!newPathname.startsWith(`/${siteGuid}/`)) {
17912                newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17913              }
17914              
17915              // Create browser URL
17916              const browserUrl = new URL(window.location.origin);
17917              browserUrl.pathname = newPathname;
17918              
17919              // Update browser URL 
17920              window.history.pushState(
17921                { customPath: newPathname },
17922                document.title,
17923                browserUrl.href
17924              );
17925              
17926              // Get the main gallery frame - NOT the iframe that sent the message
17927              const galleryFrame = document.getElementById('galleryFrame');
17928              if (galleryFrame) {
17929                // Create clean path without GUID
17930                const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
17931                
17932                // Create wrapper URL
17933                const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
17934                  `?siteId=${siteGuid}` +
17935                  `&path=${encodeURIComponent(cleanPath)}` + 
17936                  `&directPath=true` +
17937                  `&t=${Date.now()}`; // Cache buster
17938                
17939                console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl);
17940                galleryFrame.src = wrapperUrl;
17941                
17942                // Clear any active gallery ID
17943                if (window.activeGalleryId) {
17944                  window.activeGalleryId = null;
17945                }
17946                
17947                return;
17948              } else {
17949                console.warn('Gallery frame not found - cannot load content via postMessage');
17950              }
17951            }
17952          }
17953        }
17954      } catch (error) {
17955        console.error('Error processing postMessage from iframe:', error);
17956      }
17957    });
17958    
17959    // Inject a script into the iframe if possible to send messages back
17960    try {
17961      // We might be able to inject a script tag even in cross-origin scenarios
17962      // if the iframe is not yet loaded or using document.write
17963      iframe.addEventListener('load', function() {
17964        try {
17965          const script = iframe.contentDocument.createElement('script');
17966          script.textContent = `
17967            // Link interception script injected from parent
17968            document.addEventListener('click', function(e) {
17969              const link = e.target.closest('a');
17970              if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
17971                // Send message to parent window
17972                window.parent.postMessage({
17973                  type: 'linkClick',
17974                  href: link.href,
17975                  pathname: link.pathname
17976                }, '*');
17977                
17978                // Prevent default navigation
17979                e.preventDefault();
17980              }
17981            });
17982            console.log('Gallery link interceptor injected from parent');
17983          `;
17984          iframe.contentDocument.head.appendChild(script);
17985        } catch (e) {
17986          // This will likely fail for cross-origin iframes
17987          console.log('Could not inject script into iframe - expected for cross-origin frames');
17988        }
17989      });
17990    } catch (error) {
17991      console.warn('Error trying to inject script into iframe:', error);
17992    }
17993  }
17994  
17995  // Observer for new content/iframes
17996  function setupMutationObserver() {
17997    if (!window.MutationObserver) return;
17998    
17999    const observer = new MutationObserver(function(mutations) {
18000      let newIframesFound = false;
18001      
18002      mutations.forEach(function(mutation) {
18003        if (mutation.type === 'childList' && mutation.addedNodes.length) {
18004          // Check if any new iframes were added
18005          mutation.addedNodes.forEach(function(node) {
18006            // Check if the node is an iframe or contains iframes
18007            if (node.nodeName === 'IFRAME') {
18008              newIframesFound = true;
18009              setupIframeIntercept(node, node.id === 'galleryFrame');
18010            } else if (node.querySelectorAll) {
18011              const iframes = node.querySelectorAll('iframe');
18012              if (iframes.length > 0) {
18013                newIframesFound = true;
18014                iframes.forEach(function(iframe) {
18015                  setupIframeIntercept(iframe, iframe.id === 'galleryFrame');
18016                });
18017              }
18018            }
18019          });
18020        }
18021      });
18022      
18023      // If new iframes were found, update our cache
18024      if (newIframesFound) {
18025        refreshGalleryCache();
18026      }
18027    });
18028    
18029    // Observe the entire document
18030    observer.observe(document.body, {
18031      childList: true,
18032      subtree: true
18033    });
18034  }
18035  
18036  // Helper to check if a URL is a resource rather than a page
18037  function isResourceUrl(path) {
18038    const resourceExtensions = [
18039      '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 
18040      '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2'
18041    ];
18042    return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext));
18043  }
18044  
18045  // Get the site GUID from the path in preview mode
18046  function getSiteGuidFromPath() {
18047    if (window.location.hostname === 'preview.neonsky.app') {
18048      const pathParts = window.location.pathname.split('/').filter(
18048Boolean);
18049      if (pathParts.length > 0) {
18050        return pathParts[0];
18051      }
18052    }
18053    return null;
18054  }
18055  
18056
18057/**
18058 * Enhanced gallery path matching with improved accuracy:
18059 * 
18060 * This function finds the appropriate gallery based on URL path with better
18061 * prioritization and more precise matching criteria.
18062 * 
18063 * @param {string} path - The URL path to match
18064 * @param {boolean} isPreviewMode - Whether we're in preview mode
18065 * @param {string} siteGuid - The site GUID (for preview mode)
18066 * @returns {Object|null} The matching gallery or null if none found
18067 */
18068function findGalleryByPath(path, isPreviewMode, siteGuid) {
18069  // Ensure path is a string
18070  if (typeof path !== 'string') {
18071    console.warn('Invalid path provided to findGalleryByPath:', path);
18072    return null;
18073  }
18074
18075  // Determine which galleries array to use - check global variable first
18076  let galleriesData;
18077  let galleryCount = 0;
18078  
18079  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
18080    galleriesData = galleries;
18081    galleryCount = galleries.length;
18082  } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
18083    galleriesData = window.galleries;
18084    galleryCount = window.galleries.length;
18085  } else if (window.GALLERY_DATA_CACHE && 
18086             window.GALLERY_DATA_CACHE.initialized && 
18087             Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
18088    galleriesData = window.GALLERY_DATA_CACHE.galleries;
18089    galleryCount = window.GALLERY_DATA_CACHE.galleries.length;
18090  } else {
18091    console.warn('No galleries array available');
18092    return null;
18093  }
18094  
18095  console.log(`Searching for match among ${galleryCount} galleries`);
18096
18097  // Strip leading/trailing slashes without regex — template-literal embedding mangles / escapes
18098  // (e.g. /^/|/$/g becomes /^/|/$/g → 0), which left "/" unnormalized and broke home matching.
18099  function stripEdgeSlashes(value) {
18100    return String(value || '').split('/').filter(Boolean).join('/');
18101  }
18102  
18103  // Normalize the path for comparison (handle trailing/leading slashes)
18104  let normalizedPath = stripEdgeSlashes(path).toLowerCase();
18105  const originalNormalizedPath = normalizedPath; // Keep original for logging
18106  
18107  // If this is a preview URL, we need to handle the path differently
18108  if (isPreviewMode && siteGuid) {
18109    // Check if the path starts with the GUID
18110    if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) {
18111      // Remove the GUID prefix
18112      normalizedPath = normalizedPath.substring(siteGuid.length + 1);
18113    } else if (normalizedPath === siteGuid.toLowerCase()) {
18114      // This is just the GUID with no additional path
18115      normalizedPath = '';
18116    }
18117  }
18118  
18119  // Create a dash-normalized version (all sequences of dashes become single dashes)
18120  // This handles both double-dash patterns and any other dash inconsistencies
18121  const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-');
18122  
18123  console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`);
18124  
18125  // For empty paths, try to find the homepage
18126  if (!normalizedPath) {
18127    // Look for a gallery marked as home page
18128    const homePage = galleriesData.find(g => g.isHomePage === true);
18129    if (homePage) {
18130      console.log('Empty path - returning homepage:', homePage.title);
18131      return homePage;
18132    }
18133  }
18134
18135  // Array to store potential matches with their confidence levels
18136  const potentialMatches = [];
18137  
18138  // Process each gallery to find potential matches with confidence levels
18139  galleriesData.forEach(gallery => {
18140    // Skip invalid galleries
18141    if (!gallery || !gallery.id) return;
18142    
18143    // Store matching details for debugging
18144    const matchInfo = {
18145      gallery,
18146      confidence: 0,
18147      matchType: null,
18148      usedNormalizedDashes: false
18149    };
18150    
18151    // 1. EXACT SLUG MATCH (highest confidence)
18152    if (gallery.slug) {
18153      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18154      
18155      // Try original path
18156      if (gallerySlug === normalizedPath) {
18157        matchInfo.confidence = 100; // Max confidence
18158        matchInfo.matchType = 'exact_slug';
18159        potentialMatches.push({...matchInfo});
18160        return; // Continue to next gallery
18161      }
18162      
18163      // Try dash-normalized path
18164      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18165      if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) {
18166        matchInfo.confidence = 95; // Very high confidence
18167        matchInfo.matchType = 'exact_slug_normalized_dashes';
18168        matchInfo.usedNormalizedDashes = true;
18169        potentialMatches.push({...matchInfo});
18170        return; // Continue to next gallery
18171      }
18172    }
18173    
18174    // 2. EXACT URL PATH MATCH (very high confidence)
18175    if (gallery.url) {
18176      let galleryPath = gallery.url.toLowerCase();
18177      
18178      if (galleryPath.startsWith('http')) {
18179        try {
18180          const urlObj = new URL(galleryPath);
18181          galleryPath = urlObj.pathname;
18182        } catch (e) {
18183          // If URL parsing fails, use the string as is
18184        }
18185      }
18186      
18187      galleryPath = stripEdgeSlashes(galleryPath);
18188      
18189      // Try original path
18190      if (galleryPath === normalizedPath) {
18191        matchInfo.confidence = 90; // Very high confidence
18192        matchInfo.matchType = 'exact_url_path';
18193        potentialMatches.push({...matchInfo});
18194        return; // Continue to next gallery
18195      }
18196      
18197      // Try dash-normalized path
18198      const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-');
18199      if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) {
18200        matchInfo.confidence = 85; // High confidence
18201        matchInfo.matchType = 'exact_url_path_normalized_dashes';
18202        matchInfo.usedNormalizedDashes = true;
18203        potentialMatches.push({...matchInfo});
18204        return; // Continue to next gallery
18205      }
18206    }
18207    
18208    // 3. TITLE SLUG MATCH (high confidence)
18209    if (gallery.title) {
18210      const titleSlug = gallery.title.toLowerCase()
18211        .replace(/s+/g, '-')
18212        .replace(/[^w-]+/g, '')
18213        .replace(/--+/g, '-')
18214        .trim();
18215      
18216      // Try original path
18217      if (titleSlug === normalizedPath) {
18218        matchInfo.confidence = 80; // High confidence
18219        matchInfo.matchType = 'title_slug_match';
18220        potentialMatches.push({...matchInfo});
18221        return; // Continue to next gallery
18222      }
18223      
18224      // Try dash-normalized path
18225      const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-');
18226      if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) {
18227        matchInfo.confidence = 75; // Good confidence
18228        matchInfo.matchType = 'title_slug_match_normalized_dashes';
18229        matchInfo.usedNormalizedDashes = true;
18230        potentialMatches.push({...matchInfo});
18231        return; // Continue to next gallery
18232      }
18233    }
18234    
18235    // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA
18236    if (gallery.slug && normalizedPath.length > 5) {
18237      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18238      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18239      
18240      // Only if paths are substantial (to avoid false positives)
18241      if (normalizedPath.length >= 5 && gallerySlug.length >= 5) {
18242        // Try matching significant portions with dash normalization
18243        
18244        // If the normalized paths contain each other with substantial overlap
18245        if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 
18246            gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) {
18247          
18248          // Calculate overlap ratio based on the shorter string
18249          const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 
18250                               Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length);
18251          
18252          // Only consider matches with substantial overlap (over 70%)
18253          if (overlapRatio >= 0.7) {
18254            const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap
18255            
18256            matchInfo.confidence = score;
18257            matchInfo.matchType = 'partial_match_normalized';
18258            matchInfo.usedNormalizedDashes = true;
18259            matchInfo.overlapRatio = overlapRatio;
18260            potentialMatches.push({...matchInfo});
18261          }
18262        }
18263      }
18264    }
18265  });
18266  
18267  // No potential matches found
18268  if (potentialMatches.length === 0) {
18269    console.log('No gallery match found for path:', path);
18270    return null;
18271  }
18272  
18273  // Sort potential matches by confidence (highest first)
18274  potentialMatches.sort((a, b) => b.confidence - a.confidence);
18275  
18276  // Get the best match
18277  const bestMatch = potentialMatches[0];
18278  
18279  // Log detailed debugging info about the match
18280  console.log(`Best match for "${normalizedPath}":`, {
18281    title: bestMatch.gallery.title,
18282    id: bestMatch.gallery.id,
18283    matchType: bestMatch.matchType,
18284    confidence: bestMatch.confidence,
18285    usedNormalizedDashes: bestMatch.usedNormalizedDashes,
18286    totalMatches: potentialMatches.length
18287  });
18288  
18289  // If we have multiple close matches, log them for debugging
18290  if (potentialMatches.length > 1 && 
18291      potentialMatches[1].confidence >= bestMatch.confidence - 10) {
18292    console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({
18293      title: m.gallery.title,
18294      id: m.gallery.id,
18295      matchType: m.matchType,
18296      confidence: m.confidence
18297    }
18297)));
18298  }
18299  
18300  // Return the best matching gallery
18301  return bestMatch.gallery;
18302}
18303  
18304  // Make helpful functions available globally
18305  window.findGalleryByPath = findGalleryByPath;
18306  window.findGalleryById = findGalleryById;
18307  window.refreshGalleryCache = refreshGalleryCache;
18308  window.getSiteGuidFromPath = getSiteGuidFromPath;
18309  if (typeof getSiteGalleriesArray === 'function') {
18310    window.getSiteGalleriesArray = getSiteGalleriesArray;
18311  }
18312})();
18313
18314
18315
18316    </script>
18316
18317  </div>
18318  
18319<!-- Sidebar element form for adding new elements -->
18320<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet>
18321<p class="pe-edit-shell__title">Add sidebar element</p>
18322<p class="pe-hint">Select the type of element to add:</p>
18323
18324<div class="radio-group">
18325  <label>
18326    <input type="radio" name="sidebarElementType" value="text" checked> Text Block
18327  </label>
18328  <label>
18329    <input type="radio" name="sidebarElementType" value="image"> Image
18330  </label>
18331  <label>
18332    <input type="radio" name="sidebarElementType" value="social"> Social Icons
18333  </label>
18334  <label>
18335    <input type="radio" name="sidebarElementType" value="spacer"> Spacer
18336  </label>
18337</div>
18338
18339<div class="pe-footer-actions">
18340<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button>
18341<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button>
18342</div>
18343</div>
18344
18345 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet>
18346  <p class="pe-edit-shell__title">Add to menu</p>
18347  <input type="text" id="galleryTitle" placeholder="Gallery Title">
18348 <div class="radio-group">
18349   <label>
18350    <input type="radio" name="galleryType" value="integrated" checked> Gallery
18351  </label>
18352    <label>
18353    <input type="radio" name="galleryType" value="page"> Page
18354  </label>
18355  <label>
18356    <input type="radio" name="galleryType" value="folder"> Folder
18357  </label>
18358  <label>
18359    <input type="radio" name="galleryType" value="spacer"> Spacer
18360  </label>
18361    <label>
18362    <input type="radio" name="galleryType" value="external"> External URL
18363  </label>
18364</div>
18365  <div id="urlInputContainer">
18366    <input type="text" id="galleryUrl" placeholder="Gallery URL">
18367  </div>
18368  <div id="galleryParentContainer" style="display: none;" aria-hidden="true">
18369    <label for="galleryParent">Parent Menu:</label>
18370    <select id="galleryParent">
18371      <option value="">None (Top Level)</option>
18372    </select>
18373  </div>
18374  <div class="pe-footer-actions">
18375    <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button>
18376  </div>
18377    
18377<script>
18378
18379
18380      // Add event listeners to the radio buttons
18381      document.addEventListener('DOMContentLoaded', () => {
18382        const radioButtons = document.querySelectorAll('input[name="galleryType"]');
18383        radioButtons.forEach(radio => {
18384          radio.addEventListener('change', function() {
18385            const urlContainer = document.getElementById('urlInputContainer');
18386            if (this.value === 'external') {
18387              urlContainer.style.display = 'block';
18388            } else {
18389              urlContainer.style.display = 'none';
18390            }
18391          });
18392        });
18393      });
18394    </script>
18394
18395  </div>
18396
18397<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet>
18398  <h3 class="pe-edit-shell__title">Site Metadata</h3>
18399  
18400  <div class="form-group">
18401    <label for="metaTitle">Page Title (Search Results & Browsers)</label>
18402    <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results">
18403    <small>Recommended length: 50-60 characters</small>
18404  </div>
18405  
18406  <div class="form-group">
18407    <label for="metaDescription">Meta Description</label>
18408    <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea>
18409    <small>Recommended length: 150-160 characters</small>
18410  </div>
18411
18412  <div class="form-group">
18413    <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label>
18414    <input type="hidden" id="ogImageUrl" class="form-control" value="">
18415    <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn">
18416      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18417        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18418        <polyline points="17 8 12 3 7 8"></polyline>
18419        <line x1="12" y1="3" x2="12" y2="15"></line>
18420      </svg>
18421      <span id="uploadOgImageBtnText">Upload image</span>
18422    </button>
18423    <div id="ogImagePreviewContainer"></div>
18424    <small>Recommended about 1200×630px. Optional: when you upload an image here, it is used for link previews site-wide. If you leave this empty, the first image on each page or gallery (including the home page) is used instead.</small>
18425  </div>
18426  
18427  <div class="form-group">
18428    <label for="googleAnalytics">Google Analytics Code</label>
18429    <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX">
18430    <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small>
18431  </div>
18432  
18433  <div class="form-group">
18434    <label for="contentLanguage">Content Language</label>
18435    <select id="contentLanguage" class="form-control">
18436      <option value="en">English (en)</option>
18437      <option value="es">Spanish / Español (es)</option>
18438      <option value="fr">French / Français (fr)</option>
18439      <option value="ne">Nepali / नेपाली (ne)</option>
18440      <option value="de">German / Deutsch (de)</option>
18441      <option value="it">Italian / Italiano (it)</option>
18442      <option value="pt">Portuguese / Português (pt)</option>
18443      <option value="ru">Russian / Русский (ru)</option>
18444      <option value="zh">Chinese / 中文 (zh)</option>
18445      <option value="ja">Japanese / 日本語 (ja)</option>
18446      <option value="ko">Korean / 한국어 (ko)</option>
18447      <option value="ar">Arabic / العربية (ar)</option>
18448      <option value="hi">Hindi / हिन्दी (hi)</option>
18449    </select>
18450    <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small>
18451  </div>
18452  
18453  <div class="form-group">
18454    <label>
18455      <input type="checkbox" id="noIndexToggle"> 
18456      Hide site from search engines (noindex)
18457    </label>
18458    <small>Use this if the site is under development or shouldn't appear in search results</small>
18459  </div>
18460
18461  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18462    <h4 style="margin-bottom: 8px;">Schema / Identity</h4>
18463    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">Helps Google understand what this site represents — used in Knowledge Panels and local search results.</p>
18464    <label style="display: block; margin-bottom: 6px;">This site represents:</label>
18465    <div style="display: flex; gap: 20px; margin-bottom: 4px;">
18466      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18467        <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual
18468      </label>
18469      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18470        <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business
18471      </label>
18472    </div>
18473    <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small>
18474  </div>
18475
18476  <div class="form-group" id="schemaJobTitleGroup" style="display: none;">
18477    <label for="schemaJobTitle">Job Title</label>
18478    <input type="text" id="schemaJobTitle" class="form-control" placeholder="">
18479    <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small>
18480  </div>
18481
18482  <div class="form-group" id="schemaCityGroup" style="display: none;">
18483    <label for="schemaCity">City / Location</label>
18484    <input type="text" id="schemaCity" class="form-control" placeholder="">
18485    <small>Improves visibility in location-based searches.</small>
18486  </div>
18487
18488  <div class="form-group" id="schemaEmailGroup" style="display: none;">
18489    <label for="schemaEmail">Contact Email (for structured data)</label>
18490    <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]">
18491    <small>Embedded in structured data only — not displayed on the page.</small>
18492  </div>
18493
18494  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18495    <h4 style="margin-bottom: 8px;">
18495Image licensing (optional)</h4>
18496    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">Adds license URLs to gallery ImageObject structured data for Google Images. Leave both blank to keep current behavior. No badge is shown on your site.</p>
18497    <label for="imageLicenseUrl">License page URL</label>
18498    <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing">
18499    <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small>
18500  </div>
18501
18502  <div class="form-group">
18503    <label for="acquireLicensePageUrl">Acquire license page URL</label>
18504    <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact">
18505    <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small>
18506  </div>
18507
18508  <div class="form-group">
18509    <label>
18510      <input type="checkbox" id="metadataCopyrightEnabled">
18511      Enable sidebar copyright footer
18512    </label>
18513    <small>When enabled, the copyright text shows at the bottom of the sidebar.</small>
18514  </div>
18515
18516  <div class="form-group" id="metadataCopyrightTextGroup">
18517    <label for="metadataCopyrightText">Copyright Text</label>
18518    <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name">
18519    <small>This text appears in the sidebar footer and inside the copyright overlay.</small>
18520  </div>
18521  
18522  <div class="form-group">
18523    <label for="faviconUrl">Favicon</label>
18524    <input type="hidden" id="faviconUrl" class="form-control">
18525    <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn">
18526      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18527        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18528        <polyline points="17 8 12 3 7 8"></polyline>
18529        <line x1="12" y1="3" x2="12" y2="15"></line>
18530      </svg>
18531      <span id="uploadFaviconBtnText">Upload Favicon</span>
18532    </button>
18533    <div id="faviconPreviewContainer"></div>
18534    <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small>
18535  </div>
18536
18537  <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18538    <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4>
18539    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18540      Upload one self-contained HTML file (e.g. from an AI tool). When enabled, visitors to the site root see only that file — not the Hydra menu or gallery.
18541      To log in and edit, open <strong>/_manage</strong> on your domain, then press <strong>+</strong> (numpad + or Shift+= on US keyboards) to log in or toggle edit mode, then open Site Metadata from the edit bar.
18542      To log out in this browser, visit <strong>/_manage?logout=1</strong> or <strong>/_manage/logout</strong> (also works as <code>?hydra_logout=1</code> on any page).
18543    </p>
18544    <label>
18545      <input type="checkbox" id="customPublicSiteEnabled" />
18546      Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>)
18547    </label>
18548    <input type="hidden" id="customPublicSiteHtmlR2Key" value="" />
18549    <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" />
18550    <div class="form-group" style="margin-top: 10px;">
18551      <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button>
18552      <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button>
18553    </div>
18554    <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p>
18555  </div>
18556
18557  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18558    <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4>
18559    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18560      When enabled, visitors must enter this username and password in the browser’s dialog before any page or asset loads (same credentials on your live domain and on
18561      <code>preview.neonsky.app/&lt;your-site-id&gt;/</code>). Editors still use Magic login to edit. Turn off to make the site fully public again.
18562    </p>
18563    <label>
18564      <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" />
18565      Require HTTP Basic password for the entire site
18566    </label>
18567    <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;">
18568      <div class="form-group">
18569        <label for="siteWideBasicAuthUsername">Username</label>
18570        <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" />
18571      </div>
18572      <div class="form-group">
18573        <label for="siteWideBasicAuthPassword">Password</label>
18574        <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" />
18575        <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small>
18576      </div>
18577      <div class="form-group">
18578        <label for="siteWideBasicAuthRealm">Realm (optional)</label>
18579        <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" />
18580        <small>Label shown in the browser login dialog.</small>
18581      </div>
18582    </div>
18583  </div>
18584  
18585  <div class="form-actions pe-footer-actions">
18586    <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button>
18587    <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button>
18588  </div>
18589</div>
18590
18591<script>
18592var customPublicSiteOptionHeld = false;
18593var customPublicSiteOptionKeyBound = false;
18594
18595function isMetadataEditorOpen() {
18596  var editor = document.getElementById('metadataEditor');
18597  if (!editor) return false;
18598  if (window.currentOpenForm === 'metadataEditor') return true;
18599  return editor.style.display === 'block';
18600}
18601
18602function syncCustomPublicSiteBypassVisibility() {
18603  var group = document.getElementById('customPublicSiteBypassGroup');
18604  if (!group) return;
18605  var show = isMetadataEditorOpen() && customPublicSiteOptionHeld;
18606  group.hidden = !show;
18607  group.style.display = show ? 'block' : 'none';
18608}
18609
18610function bindCustomPublicSiteOptionKeyListeners() {
18611  if (customPublicSiteOptionKeyBound) return;
18612  customPublicSiteOptionKeyBound = true;
18613
18614  document.addEventListener('keydown', function(e) {
18615    if (!e.altKey) return;
18616    customPublicSiteOptionHeld = true;
18617    syncCustomPublicSiteBypassVisibility();
18618  }, true);
18619
18620  document.addEventListener('keyup', function(e) {
18621    var isOptionRelease =
18622      e.key === 'Alt' ||
18623      e.key === 'Option' ||
18624      e.code === 'AltLeft' ||
18625      e.code === 'AltRight';
18626    if (!isOptionRelease) return;
18627    customPublicSiteOptionHeld = false;
18628    syncCustomPublicSiteBypassVisibility();
18629  }, true);
18630}
18631
18632window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility;
18633window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners;
18634
18635function updateSiteWideBasicAuthFieldState() {
18636  const en = document.getElementById('siteWideBasicAuthEnabled');
18637  const fields = document.getElementById('siteWideBasicAuthFields');
18638  if (fields) fields.style.display = en && en.checked ? 'block' : 'none';
18639}
18640
18641function updateSchemaFields() {
18642  const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked;
18643  const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked;
18644  const anySelected = isPerson || isOrg;
18645  const jobTitleGroup = document.getElementById('schemaJobTitleGroup');
18646  const cityGroup = document.getElementById('schemaCityGroup');
18647  const emailGroup = document.getElementById('schemaEmailGroup');
18648  if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none';
18649  if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none';
18650  if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none';
18651}
18652
18653function uploadCustomPublicHtmlFile() {
18654  const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || '');
18655  if (!siteId) {
18656    alert('Site ID not found. Refresh the page and try again.');
18657    return;
18658  }
18659  const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null;
18660  if (!authToken) {
18661    alert('Please log in first (press + on the page).');
18662    return;
18663  }
18664  const input = document.createElement('input');
18665  input.type = 'file';
18666  input.accept = '.html,.htm,text/html';
18667  input.addEventListener('change', function() {
18668    const file = input.files && input.files[0];
18669    if (!file) return;
18670    const formData = new FormData();
18671    formData.append('file', file);
18672    formData.append('guid', siteId);
18673    formData.append('purpose', 'custom-public-site');
18674    formData.append('pageId', '_site');
18675    formData.append('elementId', 'custom-public-html');
18676    formData.append('folderPath', '');
18677    var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file';
18678    var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload;
18679    fetch(uploadUrl, {
18680      method: 'POST',
18681      headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' },
18682      body: formData
18683    }).then(function(r) {
18684      return r.text().then(function(text) {
18685        var ct = (r.headers.get('content-type') || '').toLowerCase();
18686        if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) {
18687          throw new Error(
18688            'Server returned HTML instead of JSON — POST /api/upload-file is not reaching the Hydra worker. ' +
18689            'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.'
18690          );
18691        }
18692        if (r.status === 403 && ct.indexOf('application/json') === -1) {
18693          throw new Error(
18694            'Upload blocked (403). If this is an HTML file on preview or your domain, Cloudflare WAF may be blocking the request — add a custom rule to skip WAF for POST URI path containing /api/upload-file.'
18695          );
18696        }
18697        try {
18698          return JSON.parse(text);
18699        } catch (parseErr) {
18700          throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : ''));
18701        }
18702      });
18703    }).then(function(res) {
18704      if (res.success && res.key) {
18705        var keyEl = document.getElementById('customPublicSiteHtmlR2Key');
18706        var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore');
18707        var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
18708        if (keyEl) keyEl.value = res.key;
18709        if (fnEl) fnEl.value = file.name;
18710        if (lbl) lbl.textContent = 'Current file: ' + file.name + ' — click Save Metadata below to apply.';
18711        window.siteMetadata = window.siteMetadata || {};
18712        window.siteMetadata.customPublicSiteHtmlR2Key = res.key;
18713        window.siteMetadata.customPublicSiteHtmlFilename = file.name;
18714      } else {
18715        alert(res.error || res.details || 'Upload failed');
18716      }
18717    }).catch(function(e) {
18718      alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error'));
18719    });
18720  });
18721  input.click();
18722}
18723
18724function clearCustomPublicHtmlFile() {
18725  if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return;
18726  var k = document.getElementById('customPublicSiteHtmlR2Key');
18727  var fn = document.getElementById('customPublicSiteHtmlFilenameStore');
18728  var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
18729  if (k) k.value = '';
18730  if (fn) fn.value = '';
18731  if (lbl) lbl.textContent = 'No HTML file uploaded yet.';
18732  if (window.siteMetadata) {
18733    window.siteMetadata.customPublicSiteHtmlR2Key = '';
18734    window.siteMetadata.customPublicSiteHtmlFilename = '';
18735  }
18736}
18737
18738// Handle favicon upload button click
18739document.addEventListener('DOMContentLoaded', function() {
18740  const uploadBtn = document.getElementById('uploadFaviconBtn');
18741  if (uploadBtn) {
18742    uploadBtn.addEventListener('click', function() {
18743      uploadFavicon();
18744    });
18745  }
18746  const uploadOgBtn = document.getElementById('uploadOgImageBtn');
18747  if (uploadOgBtn) {
18748    uploadOgBtn.addEventListener('click', function() {
18749      uploadSiteOgImage();
18750    });
18751  }
18752  var upHtml = document.getElementById('uploadCustomPublicHtmlBtn');
18753  if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile);
18754  var rmHtml = document.getElementById('removeCustomPublicHtmlBtn');
18755  if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile);
18756  bindCustomPublicSiteOptionKeyListeners();
18757  syncCustomPublicSiteBypassVisibility();
18758  
18759  // Handle content language dropdown change
18760  const contentLanguageSelect = document.getElementById('contentLanguage');
18761  if (contentLanguageSelect) {
18762    contentLanguageSelect.addEventListener('change', function() {
18763      console.warn('Content language changed to:', this.value);
18764      // Update global variable immediately
18765      window.siteMetadata.contentLanguage = this.value;
18766      console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage);
18767    });
18768  }
18769
18770  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
18771  if (copyrightToggle) {
18772    copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState);
18773  }
18774
18775  updateMetadataCopyrightFieldState();
18776});
18777
18778// Function to handle favicon upload using ImageUploader
18779function uploadFavicon() {
18780  console.log('Starting favicon upload...');
18781  
18782  // Check if ImageUploader is available
18783  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
18784    alert('Image uploader not available. Please enter a favicon URL manually.');
18785    return;
18786  }
18787  
18788  try {
18789    // Create options for the ImageUploader
18790    const options = {
18791      title: 'Upload Favicon',
18792      containerType: 'favicon',
18793      maxWidth: 64,  // Favicons are typically small
18794      maxHeight: 64,
18795      quality: 0.9
18796    };
18797    
18798    // Call the ImageUploader
18799    window.ImageUploader.open(options, function(imageResult, imageInfo) {
18800      console.log('Favicon upload callback received:', imageInfo);
18801      
18802      // Get the favicon URL input field
18803      let faviconInput = document.getElementById('faviconUrl');
18804      console.warn('=== UPLOAD CALLBACK DEBUG ===');
18805      console.warn('Favicon input element found:', !!faviconInput);
18806      console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A');
18807      console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A');
18808      
18809      // If element not found, wait a bit and try again (metadata editor might not be open yet)
18810      if (!faviconInput) {
18811        console.warn('Favicon input not found, waiting 100ms and trying again...');
18812        setTimeout(() => {
18813          faviconInput = document.getElementById('faviconUrl');
18814          console.warn('Retry - Favicon input element found:', !!faviconInput);
18815          if (faviconInput) {
18816            console.warn('Retry - Setting favicon input value to:', imageInfo.url);
18817            faviconInput.value = imageInfo.url;
18818            console.warn('Retry - Favicon input value after setting:', faviconInput.value);
18819          } else {
18820            console.error('Retry - Favicon input field still not found');
18821          }
18822        }, 100);
18823        return;
18824      }
18825      
18826      // Set the uploaded image URL
18827      if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) {
18828        // R2 storage - use the URL
18829        console.warn('Setting favicon input value to:', imageInfo.url);
18830        faviconInput.value = imageInfo.url;
18831        console.warn('Favicon input value after setting:', faviconInput.value);
18832        
18833        // Also store in global variable as backup
18834        window.uploadedFaviconUrl = imageInfo.url;
18835        console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl);
18836        
18837        console.log('Favicon URL set to:', imageInfo.url);
18838        
18839        // Update button text
18840        const btnText = document.getElementById('uploadFaviconBtnText');
18841        if (btnText) {
18842          btnText.textContent = 'Change Favicon';
18843        }
18844        
18845        // Show preview
18846        showFaviconPreview(imageInfo.url);
18847      } else if (imageResult) {
18848        // Legacy base64 storage
18849        faviconInput.value = imageResult;
18850        console.log('Favicon set to base64 data');
18851        
18852        // Update button text
18853        const btnText = document.getElementById('uploadFaviconBtnText');
18854        if (btnText) {
18855          btnText.textContent = 'Change Favicon';
18856        }
18857        
18858        // Show preview
18859        showFaviconPreview(imageResult);
18860      } else {
18861        console.error('No image data received from uploader');
18862        alert('Upload failed. Please try again.');
18863        return;
18864      }
18865      
18866      // Show success message
18867      console.log('Favicon uploaded successfully');
18868    });
18869    
18870  } catch (error) {
18871    console.error('Error opening favicon uploader:', error);
18872    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
18873  }
18874}
18875
18876// Function to show a preview of the uploaded favicon
18877function showFaviconPreview(imageUrl) {
18878  const container = document.getElementById('faviconPreviewContainer');
18879  if (!container) {
18880    console.error('Favicon preview container not found');
18881    return;
18882  }
18883  
18884  container.innerHTML = 
18885    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
18886      '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' +
18887      '<div style="flex: 1;">' +
18888        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' +
18889        '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' +
18890      '</div>' +
18891      '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
18892        'Remove' +
18893      '</button>' +
18894    '</div>';
18895}
18896
18897// Function to remove the favicon
18898function removeFavicon() {
18899  const faviconInput = document.getElementById('faviconUrl');
18900  if (faviconInput) {
18901    faviconInput.value = '';
18902  }
18903  
18904  const container = document.getElementById('faviconPreviewContainer');
18905  if (container) {
18906    container.innerHTML = '';
18907  }
18908  
18909  const btnText = document.getElementById('uploadFaviconBtnText');
18910  if (btnText) {
18911    btnText.textContent = 'Upload Favicon';
18912  }
18913  
18914  console.log('Favicon removed');
18915}
18916
18917function uploadSiteOgImage() {
18918  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
18919    alert('Image uploader not available. Refresh the page and try again.');
18920    return;
18921  }
18922  try {
18923    window.ImageUploader.open({
18924      title: 'Upload social preview image',
18925      containerType: 'page',
18926      quality: 0.92
18927    }, function(imageResult, imageInfo) {
18928      const inp = document.getElementById('ogImageUrl');
18929      const setUrl = function(url) {
18930        if (inp) inp.value = url;
18931        const btnText = document.getElementById('uploadOgImageBtnText');
18932        if (btnText) btnText.textContent = 'Change image';
18933        showOgImagePreview(url);
18934        window.siteMetadata = window.siteMetadata || {};
18935        window.siteMetadata.ogImageUrl = url;
18936      };
18937      if (imageInfo && imageInfo.url) {
18938        setUrl(imageInfo.url);
18939      } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) {
18940        setUrl(imageResult);
18941      } else {
18942        alert('Upload failed. Please try again.');
18943      }
18944    });
18945  } catch (error) {
18946    console.error('Error opening OG image uploader:', error);
18947    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
18948  }
18949}
18950
18951function showOgImagePreview(imageUrl) {
18952  const container = document.getElementById('ogImagePreviewContainer');
18953  if (!container) return;
18954  container.innerHTML =
18955    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
18956      '<img src="' + imageUrl + '" alt="Social preview" style="max-width: 200px; max-height: 105px; width: auto; height: auto; border: 1px solid #ccc; object-fit: contain; background: white;">' +
18957      '<div style="flex: 1;">' +
18958        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' +
18959        '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' +
18960      '</div>' +
18961      '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
18962        'Remove' +
18963      '</button>' +
18964    '</div>';
18965}
18966
18967function removeOgImage() {
18968  const inp = document.getElementById('ogImageUrl');
18969  if (inp) inp.value = '';
18970  if (window.siteMetadata) window.siteMetadata.ogImageUrl = '';
18971  const container = document.getElementById('ogImagePreviewContainer');
18972  if (container) container.innerHTML = '';
18973  const btnText = document.getElementById('uploadOgImageBtnText');
18974  if (btnText) btnText.textContent = 'Upload image';
18975}
18976</script>
18976
18977
18978  <!-- Import Classic Form -->
18979<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet>
18980  <h3>Import Galleries from JSON or Classic GUID</h3>
18981  <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p>
18982  
18983  <div class="form-group">
18984    <label for="pastedJson">Pasted Site JSON:</label>
18985    <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea>
18986  </div>
18987
18988  <div class="import-separator">OR</div>
18989
18990  <div class="form-group">
18991    <label for="classicGuid">Classic GUID or Import ID:</label>
18992    <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)">
18993    <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small>
18994  </div>
18995
18996    <div class="form-group">
18997      <label>
18998        <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;">
18999        Replace existing menu structure
19000      </label>
19001      <small>If checked, the current menu will be deleted and replaced with the imported items. If unchecked, imported items will be added to the current menu.</small>
19002  </div>
19003
19004  <div class="form-group">
19005    <label for="importParent">Import under:</label>
19006    <select id="importParent">
19007      <option value="">Top Level (No Parent)</option>
19008    </select>
19009  </div>
19010
19011  <div class="form-group">
19012    <label>
19013      <input type="checkbox" id="createSubmenu" checked>
19014      Create submenu for all imported galleries
19015    </label>
19016  </div>
19017
19018  <div class="form-group" id="submenuTitleContainer">
19019    <label for="submenuTitle">Submenu title:</label>
19020    <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries">
19021  </div>
19022
19023  <div id="importStatus" class="import-status" style="display: none;">
19024    <div class="import-progress"><div class="progress-bar"></div></div>
19025    <div class="status-message">Processing...</div>
19026  </div>
19027
19028  <div class="form-actions">
19029    <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button>
19030    <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button>
19031    <button class="btn" onclick="toggleImportClassicForm()">Cancel</button>
19032  </div>
19033</div>
19034  <div class="tree-container">
19035    <div id="galleryTree" data-help-id="menu-tree"></div>
19036  </div>
19037
19038    <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js -->
19039  <div class="sidebar-elements-container"></div>
19040</div>
19041<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1">
19042  <iframe class="gallery-iframe" id="galleryFrame" title="Podcast"></iframe>
19043</div>
19044</div>
19045
19046<!-- Cookie Notice (compliance mode — only when Display Cookie Notice is enabled) -->
19047
19048
19049<script>
19050// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized
19051(function() {
19052  let hasRevealed = false; // Ensure we only reveal once per page load
19053  
19054  function revealContent() {
19055    if (hasRevealed) return; // Prevent multiple calls
19056    hasRevealed = true;
19057    document.documentElement.classList.add('hydra-ready');
19058    document.body.classList.add('hydra-ready');
19059    console.log('✅ Content revealed - hydra-ready class added');
19060  }
19061  
19062  // Check if this page has a gallery container
19063  function hasGalleryContainer() {
19064    return !!document.getElementById('neon-gallery-container') || 
19065           !!document.querySelector('.gallery-wrapper') ||
19066           !!document.querySelector('.masonry-container');
19067  }
19068  
19069  // Check if gallery is already initialized
19070  function isGalleryInitialized() {
19071    return window.neonGalleryState?.initialized || 
19072           window.neonGalleryState?.domInitialized ||
19073           (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized());
19074  }
19075  
19076  // Main reveal logic
19077  function attemptReveal() {
19078    const hasGallery = hasGalleryContainer();
19079    const galleryInitialized = isGalleryInitialized();
19080    
19081    // If no gallery, reveal immediately after CSS loads
19082    if (!hasGallery) {
19083      if (document.readyState === 'complete') {
19084        revealContent();
19085      } else {
19086        window.addEventListener('load', revealContent, { once: true });
19087      }
19088      return;
19089    }
19090    
19091    // If gallery exists and is initialized, reveal
19092    if (hasGallery && galleryInitialized) {
19093      revealContent();
19094      return;
19095    }
19096    
19097    // If gallery exists but not initialized, wait for initialization
19098    if (hasGallery && !galleryInitialized) {
19099      console.log('⏳ Waiting for gallery initialization before revealing content...');
19100      
19101      // Listen for gallery-loaded event
19102      window.addEventListener('gallery-loaded', function() {
19103        console.log('✅ Gallery-loaded event received');
19104        setTimeout(revealContent, 100); // Small delay to ensure DOM is updated
19105      }, { once: true });
19106      
19107      // Listen for page-loaded event
19108      window.addEventListener('page-loaded', function() {
19109        console.log('✅ Page-loaded event received');
19110        setTimeout(revealContent, 100);
19111      }, { once: true });
19112      
19113      // Fallback: Poll for gallery initialization (max 10 seconds)
19114      let pollAttempts = 0;
19115      const maxPollAttempts = 100; // 10 seconds (100 * 100ms)
19116      const pollInterval = setInterval(function() {
19117        pollAttempts++;
19118        if (isGalleryInitialized()) {
19119          console.log('✅ Gallery initialized detected via polling');
19120          clearInterval(pollInterval);
19121          revealContent();
19122        } else if (pollAttempts >= maxPollAttempts) {
19123          console.warn('⚠️ Gallery initialization timeout - revealing content anyway');
19124          clearInterval(pollInterval);
19125          revealContent(); // Reveal anyway to prevent blank screen
19126        }
19127      }, 100);
19128      
19129      // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen)
19130      setTimeout(function() {
19131        if (!hasRevealed) {
19132          console.warn('⚠️ Safety timeout reached - revealing content to prevent blank screen');
19133          revealContent();
19134        }
19135      }, 8000);
19136    }
19137  }
19138  
19139  // Start the reveal process
19140  if (document.readyState === 'loading') {
19141    document.addEventListener('DOMContentLoaded', function() {
19142      // Wait a bit for scripts to initialize
19143      setTimeout(attemptReveal, 100);
19144    });
19145  } else if (document.readyState === 'interactive' || document.readyState === 'complete') {
19146    // DOM already ready, wait a bit for scripts
19147    setTimeout(attemptReveal, 100);
19148  } else {
19149    // Fallback
19150    window.addEventListener('load', attemptReveal, { once: true });
19151  }
19152})();
19153</script>
19153
19154
19155<script>
19156// ============================================
19157// LANGUAGE TRANSLATION SYSTEM
19158// ============================================
19159
19160(function() {
19161  // CRITICAL: Only initialize translation system if a languages element exists
19162  // This ensures sites without languages element have ZERO overhead
19163  const checkForLanguagesElement = function() {
19164    // Wait for SidebarManager to be initialized
19165    if (!window.SidebarManager || !window.SidebarManager.elements) {
19166      return false;
19167    }
19168    
19169    // Check if any languages element exists
19170    const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages');
19171    
19172    if (!hasLanguagesElement) {
19173      console.warn('No languages element found - translation system disabled');
19174      return false;
19175    }
19176    
19177    return true;
19178  };
19179  
19180  // Check for languages element, retry if SidebarManager not ready yet
19181  let initAttempts = 0;
19182  const maxAttempts = 10;
19183  
19184  function tryInitialize() {
19185    if (checkForLanguagesElement()) {
19186      console.warn('✅ Languages element found - initializing translation system');
19187      initializeTranslationSystem();
19188    } else {
19189      initAttempts++;
19190      if (initAttempts < maxAttempts) {
19191        // Retry in 500ms (total max wait: 5 seconds)
19192        setTimeout(tryInitialize, 500);
19193      } else {
19194        console.warn('Translation system not needed for this site - no languages element found after 5 seconds');
19195      }
19196    }
19197  }
19198  
19199  // Start checking after initial delay
19200  setTimeout(tryInitialize, 500);
19201  
19202  function initializeTranslationSystem() {
19203    console.warn('✅ Translation system initialized successfully');
19204    
19205    let currentLanguage = localStorage.getItem('hydra_language') || 'en';
19206    const originalTexts = new Map();
19207    
19208    // Expose test function for debugging
19209    window.testTranslation = function(langCode) {
19210      console.warn('🧪 Manual translation test triggered for:', langCode);
19211      console.warn('🔍 Dispatching test event...');
19212      window.dispatchEvent(new CustomEvent('hydra:language-change', {
19213        detail: { code: langCode || 'es', name: 'Test' }
19214      }));
19215      console.warn('✅ Test event dispatched - check if listener caught it above');
19216    };
19217    
19218    console.warn('✅ Event listener for "hydra:language-change" is now active');
19219    
19220    // Watch for edit mode changes and restore default language
19221    const editModeObserver = new MutationObserver(function(mutations) {
19222    mutations.forEach(function(mutation) {
19223      if (mutation.attributeName === 'class') {
19224        const isEditMode = document.body.classList.contains('edit-mode-active');
19225        if (isEditMode) {
19226          console.warn('Edit mode activated - restoring default language');
19227          // Restore original text
19228          restoreOriginalText();
19229          
19230          // Update language buttons to show default language
19231          const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19232          const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19233          
19234          document.querySelectorAll('.language-button-live').forEach(btn => {
19235            if (btn.getAttribute('data-lang-code') === defaultLang) {
19236              btn.style.background = 'var(--menu-active-color, #4682B4)';
19237            } else {
19238              btn.style.background = 'transparent';
19239            }
19240          });
19241        }
19242      }
19243    });
19244  });
19245  
19246  // Start observing body for class changes
19247  if (document.body) {
19248    editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
19249  }
19250  
19251  // Watch for dynamically loaded content (galleries, pages, etc.) and translate it
19252  const contentObserver = new MutationObserver(function(mutations) {
19253    // Only process if we're not in default language and not in edit mode
19254    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19255    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19256    const isEditMode = document.body.classList.contains('edit-mode-active');
19257    
19258    if (currentLanguage === defaultLang || isEditMode) {
19259      return; // Don't translate if in default language or edit mode
19260    }
19261    
19262    // Check if new text content was added
19263    let hasNewTextContent = false;
19264    mutations.forEach(function(mutation) {
19265      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
19266        mutation.addedNodes.forEach(function(node) {
19267          if (node.nodeType === Node.ELEMENT_NODE) {
19268            // Check if the added element contains text we should translate
19269            const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text');
19270            if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) {
19271              hasNewTextContent = true;
19272            }
19273          }
19274        });
19275      }
19276    });
19277    
19278    if (hasNewTextContent) {
19279      console.warn('New content detected - translating to:', currentLanguage);
19280      // Debounce to avoid translating multiple times rapidly
19281      clearTimeout(window._translationDebounce);
19282      window._translationDebounce = setTimeout(function() {
19283        translatePage(currentLanguage, defaultLang);
19284      }, 300);
19285    }
19286  });
19287  
19288  // Observe the gallery container and page content areas for new content
19289  const galleryContainer = document.querySelector('.gallery-container');
19290  if (galleryContainer) {
19291    contentObserver.observe(galleryContainer, { 
19292      childList: true, 
19293      subtree: true 
19294    });
19295  }
19296  
19297  // Also observe the main content area
19298  const mainContent = document.querySelector('.container');
19299  if (mainContent) {
19300    contentObserver.observe(mainContent, { 
19301      childList: true, 
19302      subtree: true 
19303    });
19304  }
19305  
19306  // Listen for language change events
19307  window.addEventListener('hydra:language-change', async function(e) {
19308    const { code, name } = e.detail;
19309    console.warn('🌐 Language changed to:', code, name);
19310    
19311    // CRITICAL: Ignore language changes in edit mode
19312    if (document.body.classList.contains('edit-mode-active')) {
19313      console.warn('⚠️ Edit mode active - ignoring language change');
19314      // Reset to default language
19315      const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19316      const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19317      
19318      // Update button states to show default language
19319      document.querySelectorAll('.language-button-live').forEach(btn => {
19320        if (btn.getAttribute('data-lang-code') === defaultLang) {
19321          btn.style.background = 'var(--menu-active-color, #4682B4)';
19322        } else {
19323          btn.style.background = 'transparent';
19324        }
19325      });
19326      
19327      // Show message to user
19328      console.warn('Language switching is disabled in edit mode');
19329      return;
19330    }
19331    
19332    // Update all language buttons
19333    document.querySelectorAll('.language-button-live').forEach(btn => {
19334      if (btn.getAttribute('data-lang-code') === code) {
19335        btn.style.background = 'var(--menu-active-color, #4682B4)';
19336      } else {
19337        btn.style.background = 'transparent';
19338      }
19339    });
19340    
19341    // Save preference
19342    localStorage.setItem('hydra_language', code);
19343    currentLanguage = code;
19344    
19345    // Get default language from languages element
19346    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19347    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19348    
19349    if (code === defaultLang) {
19350      // Restore original language
19351      restoreOriginalText();
19352    } else {
19353      // Translate page
19354      await translatePage(code, defaultLang);
19355    }
19356  });
19357  
19358  async function translatePage(targetLang, sourceLang) {
19359    console.warn(`🔄 Translating page from ${sourceLang} to ${targetLang}`);
19360    
19361    // Elements to translate
19362    const selectors = [
19363      '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode)
19364      '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode)
19365      'h1:not(.edit-mode-active h1)', 
19366      'h2:not(.edit-mode-active h2)', 
19367      'h3:not(.edit-mode-active h3)',
19368      'p:not(.edit-mode-active p)'
19369    ];
19370    
19371    const elements = document.querySelectorAll(selectors.join(', '));
19372    
19373    for (const element of elements) {
19374      // Skip if already translated to this language
19375      if (element.dataset.translatedTo === targetLang) continue;
19376      
19377      // Skip if element is empty
19378      const text = element.textContent.trim();
19379      if (!text || text.length === 0) continue;
19380      
19381      // Store original text
19382      if (!originalTexts.has(element)) {
19383        originalTexts.set(element, text);
19384      }
19385      
19386      // Check translation cache
19387      const cacheKey = `translate_${text}_${targetLang}`;
19388      let translated = getFromCache(cacheKey);
19389      
19390      if (translated) {
19391        element.textContent = translated;
19392        element.dataset.translatedTo = targetLang;
19393      } else {
19394        // Translate
19395        try {
19396          translated = await translate(text, sourceLang, targetLang);
19397          if (translated && translated !== text) {
19398            element.textContent = translated;
19399            element.dataset.translatedTo = targetLang;
19400            saveToCache(cacheKey, translated);
19401          }
19402        } catch (error) {
19403          console.error('Translation error:', error);
19404        }
19405      }
19406    }
19407  }
19408  
19409  function restoreOriginalText() {
19410    originalTexts.forEach((originalText, element) => {
19411      if (element && element.isConnected) {
19412        element.textContent = originalText;
19413        delete element.dataset.translatedTo;
19414      }
19415    });
19416  }
19417  
19418  async function translate(text, sourceLang, targetLang) {
19419    if (!text || text.trim().length === 0) return text;
19420    
19421    // Try Browser Translation API (Free, Chrome 130+)
19422    if ('translation' in self && 'createTranslator' in self.translation) {
19423      try {
19424        console.warn('🔄 Using Browser Translation API');
19425        const translator = await self.translation.createTranslator({
19426          sourceLanguage: sourceLang,
19427          targetLanguage: targetLang
19428        });
19429        const result = await translator.translate(text);
19430        console.warn('✅ Translation successful via Browser API');
19431        return result;
19432      } catch (error) {
19433        console.warn('❌ Browser translation failed:', error);
19434      }
19435    } else {
19436      console.warn('⚠️ Browser Translation API not available');
19437      console.warn('   To enable in Chrome: chrome://flags/#translation-api');
19438      console.warn('   OR add Google Translate API key to use paid translation');
19439    }
19440    
19441    // TODO: Add Google Translate API fallback here
19442    // For now, return original text
19443    console.warn('⚠️ No translation provider configured - returning original text');
19444    return text;
19445  }
19446  
19447  // Cache management
19448  function getFromCache(key) {
19449    try {
19450      const item = localStorage.getItem(`hydra_${key}`);
19451      if (!item) return null;
19452      
19453      const { value, expiry } = JSON.parse(item);
19454      if (expiry && Date.now() > expiry) {
19455        localStorage.removeItem(`hydra_${key}`);
19456        return null;
19457      }
19458      return value;
19459    } catch {
19460      return null;
19461    }
19462  }
19463  
19464  function saveToCache(key, value) {
19465    try {
19466      const item = {
19467        value,
19468        expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
19469      };
19470      localStorage.setItem(`hydra_${key}`, JSON.stringify(item));
19471    } catch (error) {
19472      console.warn('Cache save failed:', error);
19473    }
19474  }
19475  
19476  } // End of initializeTranslationSystem
19477})();
19478</script>
19478
19479
19480
19481
19482<script>
19483window.HYDRA_HELP_ENABLED = true;
19484function loadHelpSystemScript() {
19485  if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve();
19486  return new Promise(function(resolve, reject) {
19487    var existing = document.querySelector('script[data-hydra-help-system]');
19488    if (existing) {
19489      existing.addEventListener('load', function() { resolve(); });
19490      existing.addEventListener('error', reject);
19491      return;
19492    }
19493    var s = document.createElement('script');
19494    s.src = '/api/help-system.js?v=686c85345749';
19495    s.async = true;
19496    s.setAttribute('data-hydra-help-system', '1');
19497    s.onload = function() { resolve(); };
19498    s.onerror = reject;
19499    document.head.appendChild(s);
19500  });
19501}
19502function openHydraHelp(opts) {
19503  opts = opts || {};
19504  if (!window.HYDRA_HELP_ENABLED) return;
19505  var token = window.didToken || localStorage.getItem('hydra_auth_token');
19506  var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
19507  if (!token || !isAdmin) return;
19508  var p = loadHelpSystemScript();
19509  Promise.resolve(p).then(function() {
19510    if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init();
19511  }).then(function() {
19512    if (window.HelpSystem && window.HelpSystem.openHub) {
19513      window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined);
19514    }
19515  }).catch(function(err) {
19516    console.error('HelpSystem init failed:', err);
19517  });
19518}
19519window.openHydraHelp = openHydraHelp;
19520</script>
19520
19521
19522<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"5f8c17677ba84f8d88ba3d46ef47b608","spa":2}' crossorigin="anonymous"></script>
19522
19523</body>
19524</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.