PageSourceSearch

https://eiriniphoto.com/book

html eiriniphoto.com collected 2026-10-06 21:33:58 UTC 916,325 bytes, 19,735 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="q7u17cy4">
7  
8  <!-- Primary Meta Tags -->
9  <title>Book | Eirini Vourloumis</title>
10  <meta name="description" content="In Waiting
11
12Published by Hatje Cantz
13Release Date,October 2017
14Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis
15100 pg, 42 ills.
16hardcover
179.00…">
18  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
19  <meta name="author" content="eiriniphoto.com">
20  <meta name="language" content="en">
21  
22  <!-- Canonical URL -->
23  <link rel="canonical" href="https://eiriniphoto.com/book">
24  
25  <!-- Favicon -->
26  
27  
28  
29  
30  
31  <!-- Open Graph / Facebook / iMessage / Discord -->
32  <meta property="og:type" content="article">
33  <meta property="og:url" content="https://eiriniphoto.com/book">
34  <meta property="og:title" content="Book | Eirini Vourloumis">
35  <meta property="og:description" content="In Waiting
36
37Published by Hatje Cantz
38Release Date,October 2017
39Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis
40100 pg, 42 ills.
41hardcover
429.00…">
43  <meta property="og:site_name" content="eiriniphoto.com">
44  <meta property="og:locale" content="en_US">
45  <meta property="og:image" content="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg">
46  <meta property="og:image:secure_url" content="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg">
47  <meta property="og:image:width" content="1200">
48  <meta property="og:image:height" content="630">
49  <meta property="og:image:type" content="image/jpeg">
50  <meta property="og:image:alt" content="Book | Eirini Vourloumis">
51  
52  <!-- Twitter / X Card -->
53  <meta name="twitter:card" content="summary_large_image">
54  <meta name="twitter:site" content="@eiriniphoto.com">
55  <meta name="twitter:url" content="https://eiriniphoto.com/book">
56  <meta name="twitter:title" content="Book | Eirini Vourloumis">
57  <meta name="twitter:description" content="In Waiting
58
59Published by Hatje Cantz
60Release Date,October 2017
61Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis
62100 pg, 42 ills.
63hardcover
649.00…">
65  <meta name="twitter:image" content="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg">
66  <meta name="twitter:image:alt" content="Book | Eirini Vourloumis">
67  
68  <!-- Additional SEO Meta Tags -->
69  <meta name="theme-color" content="#000000">
70  <meta name="msapplication-TileColor" content="#000000">
71  <meta name="apple-mobile-web-app-capable" content="yes">
72  <meta name="apple-mobile-web-app-status-bar-style" content="default">
73  <meta name="apple-mobile-web-app-title" content="Book | Eirini Vourloumis">
74  
75  <!-- Security Headers -->
76  <meta http-equiv="X-Content-Type-Options" content="nosniff">
77  <meta http-equiv="X-XSS-Protection" content="1; mode=block">
78  <style>
79  /* FOUC gate: opacity on <html> cannot be pierced by child visibility:!important (submenu CSS) */
80  html:not(.hydra-ready) { opacity: 0 !important; }
81  /* Critical: hide nested menus before menu-styles.css arrives */
82  li:not(.expanded) > ol.nested-sortable { display: none !important; }
83  .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; }
84  .hydra-seo-nav ul { list-style:none;margin:0;padding:0; }
85  .hydra-skip-link { position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden; }
86  </style>
87  <noscript><style>html { opacity: 1 !important; }</style></noscript>
88  
88<script type="application/ld+json">
89  {
90  "@context": "https://schema.org",
91  "@type": [
92    "WebPage",
93    "ImageGallery"
94  ],
95  "name": "Book | Eirini Vourloumis",
96  "description": "In Waiting\\n\\nPublished by Hatje Cantz\\nRelease Date,October 2017\\nText(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis\\n100 pg, 42 ills.\\nhardcover\\n9.00…",
97  "url": "https://eiriniphoto.com/book",
98  "datePublished": "2026-10-06T21:33:58.466Z",
99  "dateModified": "2026-10-06T21:33:58.466Z",
100  "inLanguage": "en-US",
101  "isPartOf": {
102    "@type": "WebSite",
103    "@id": "https://eiriniphoto.com/#website",
104    "name": "eiriniphoto.com",
105    "url": "https://eiriniphoto.com/",
106    "description": "In Waiting\\n\\nPublished by Hatje Cantz\\nRelease Date,October 2017\\nText(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis\\n100 pg, 42 ills.\\nhardcover\\n9.00…",
107    "publisher": {
108      "@type": "Organization",
109      "name": "eiriniphoto.com",
110      "url": "https://eiriniphoto.com/"
111    }
112  },
113  "breadcrumb": {
114    "@type": "BreadcrumbList",
115    "itemListElement": [
116      {
117        "@type": "ListItem",
118        "position": 1,
119        "name": "Home",
120        "item": "https://eiriniphoto.com/"
121      },
122      {
123        "@type": "ListItem",
124        "position": 2,
125        "name": "Book | Eirini Vourloumis",
126        "item": "https://eiriniphoto.com/book"
127      }
128    ]
129  },
130  "associatedMedia": [
131    {
132      "@type": "ImageObject",
133      "name": "IW1",
134      "description": "",
135      "caption": "IW1",
136      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg"
137    },
138    {
139      "@type": "ImageObject",
140      "name": "IW2",
141      "description": "",
142      "caption": "IW2",
143      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW2.jpg"
144    },
145    {
146      "@type": "ImageObject",
147      "name": "IW3",
148      "description": "",
149      "caption": "IW3",
150      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW3.jpg"
151    },
152    {
153      "@type": "ImageObject",
154      "name": "IW4",
155      "description": "",
156      "caption": "IW4",
157      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW4.jpg"
158    },
159    {
160      "@type": "ImageObject",
161      "name": "IW5",
162      "description": "",
163      "caption": "IW5",
164      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW5.jpg"
165    },
166    {
167      "@type": "ImageObject",
168      "name": "IW6",
169      "description": "",
170      "caption": "IW6",
171      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW6.jpg"
172    },
173    {
174      "@type": "ImageObject",
175      "name": "IW7",
176      "description": "",
177      "caption": "IW7",
178      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW7.jpg"
179    },
180    {
181      "@type": "ImageObject",
182      "name": "IW8",
183      "description": "",
184      "caption": "IW8",
185      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW8.jpg"
186    },
187    {
188      "@type": "ImageObject",
189      "name": "IW9",
190      "description": "",
191      "caption": "IW9",
192      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW9.jpg"
193    },
194    {
195      "@type": "ImageObject",
196      "name": "IW10",
197      "description": "",
198      "caption": "IW10",
199      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW10.jpg"
200    },
201    {
202      "@type": "ImageObject",
203      "name": "IW11",
204      "description": "",
205      "caption": "IW11",
206      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW11.jpg"
207    },
208    {
209      "@type": "ImageObject",
210      "name": "IW12",
211      "description": "",
212      "caption": "IW12",
213      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW12.jpg"
214    },
215    {
216      "@type": "ImageObject",
217      "name": "IW13",
218      "description": "",
219      "caption": "IW13",
220      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW13.jpg"
221    },
222    {
223      "@type": "ImageObject",
224      "name": "IW14",
225      "description": "",
226      "caption": "IW14",
227      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW14.jpg"
228    },
229    {
230      "@type": "ImageObject",
231      "name": "IW15",
232      "description": "",
233      "caption": "IW15",
234      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW15.jpg"
235    },
236    {
237      "@type": "ImageObject",
238      "name": "IW16",
239      "description": "",
240      "caption": "IW16",
241      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW16.jpg"
242    },
243    {
244      "@type": "ImageObject",
245      "name": "IW17",
246      "description": "",
247      "caption": "IW17",
248      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW17.jpg"
249    },
250    {
251      "@type": "ImageObject",
252      "name": "IW18",
253      "description": "",
254      "caption": "IW18",
255      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW18.jpg"
256    },
257    {
258      "@type": "ImageObject",
259      "name": "IW19w",
260      "description": "",
261      "caption": "IW19w",
262      "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW19w.jpg"
263    }
264  ],
265  "numberOfItems": 19,
266  "primaryImageOfPage": {
267    "@type": "ImageObject",
268    "url": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg",
269    "width": 1200,
270    "height": 630,
271    "caption": "Book | Eirini Vourloumis"
272  },
273  "image": {
274    "@type": "ImageObject",
275    "url": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg",
276    "width": 1200,
277    "height": 630,
278    "caption": "Book | Eirini Vourloumis"
279  }
280}
281  </script>
281
282  
283  
284  <!-- Preview mode helper script -->
285  
285<script>
286    // Immediately execute to patch request methods before any other script runs
287    (function() {
288      // Detect preview mode by checking hostname
289      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
290      
291      if (isPreviewMode) {
292        console.log("[Preview Helper] Preview mode detected");
293        
294        // Extract GUID from URL path (first segment)
295        const pathParts = window.location.pathname.split('/').filter(Boolean);
296        const previewGuid = pathParts.length > 0 ? pathParts[0] : '';
297        
298        if (!previewGuid) {
299          console.warn("[Preview Helper] No GUID found in path:", window.location.pathname);
300          return; // Exit if no GUID found
301        }
302        
303        console.log("[Preview Helper] Using GUID:", previewGuid);
304        
305        // Store these in window for other scripts
306        window.isPreviewMode = true;
307        window.previewGuid = previewGuid;
308        window.isHydraEnvironment = true;
309        window.Parameters = window.Parameters || {};
310        window.Parameters.isHydra = true;
311        
312        // Helper function that adds the GUID to API URLs and relative links
313        window.getApiUrl = function(endpoint) {
314          // For relative paths
315          if (endpoint.startsWith('/api/')) {
316            const newUrl = '/' + previewGuid + endpoint;
317            console.log("[Preview Helper] Transformed API URL:", endpoint, "→", newUrl);
318            return newUrl;
319          }
320          return endpoint;
321        };
322        
323        // Helper function that adds the GUID to any relative link
324        window.getPreviewUrl = function(url) {
325          // For relative paths (starts with /) that aren't already prefixed with GUID
326          if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
327            const newUrl = '/' + previewGuid + url;
328            console.log("[Preview Helper] Transformed relative link:", url, "→", newUrl);
329            return newUrl;
330          }
331          return url;
332        };
333        
334        // PATCH FETCH API with enhanced URL handling
335        const originalFetch = window.fetch;
336        window.fetch = function(resource, init) {
337          // Only transform string URLs
338          if (typeof resource === 'string') {
339            // Case 1: Handle relative paths starting with /api/
340            if (resource.startsWith('/api/')) {
341              const newUrl = window.getApiUrl(resource);
342              console.log("[Preview Helper] Patched fetch call (API):", resource, "→", newUrl);
343              return originalFetch(newUrl, init);
344            }
345            
346            // Case 2: Handle other relative paths (for content/gallery links)
347            else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) {
348              const newUrl = window.getPreviewUrl(resource);
349              console.log("[Preview Helper] Patched fetch call (relative):", resource, "→", newUrl);
350              return originalFetch(newUrl, init);
351            }
352            
353            // Case 3: Handle absolute URLs to the same domain containing /api/
354            else if (resource.includes(window.location.host) && resource.includes('/api/')) {
355              try {
356                const urlObj = new URL(resource);
357                if (urlObj.hostname === window.location.hostname) {
358                  const path = urlObj.pathname;
359                  // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) — do not double-prefix
360                  if (path.startsWith('/' + previewGuid + '/')) {
361                    return originalFetch(resource, init);
362                  }
363                  const newPath = '/' + previewGuid + path;
364                  urlObj.pathname = newPath;
365                  const newUrl = urlObj.toString();
366                  console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "→", newUrl);
367                  return originalFetch(newUrl, init);
368                }
369              } catch (e) {
370                console.error("[Preview Helper] Error processing URL:", e);
371              }
372            }
373            
374            // Case 4: Handle absolute URLs to the same domain (non-API)
375            else if (resource.includes(window.location.host)) {
376              try {
377                const urlObj = new URL(resource);
378                if (urlObj.hostname === window.location.hostname && 
379                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
380                  const path = urlObj.pathname;
381                  const newPath = '/' + previewGuid + path;
382                  urlObj.pathname = newPath;
383                  const newUrl = urlObj.toString();
384                  console.log("[Preview Helper] Patched fetch call (absolute):", resource, "→", newUrl);
385                  return originalFetch(newUrl, init);
386                }
387              } catch (e) {
388                console.error("[Preview Helper] Error processing URL:", e);
389              }
390            }
391          }
392          return originalFetch(resource, init);
393        };
394        console.log("[Preview Helper] Patched fetch API");
395        
396        // PATCH XMLHttpRequest with enhanced URL handling
397        const originalOpen = XMLHttpRequest.prototype.open;
398        XMLHttpRequest.prototype.open = function(method, url, ...rest) {
399          // Only transform string URLs
400          if (typeof url === 'string') {
401            // Case 1: Handle relative paths starting with /api/
402            if (url.startsWith('/api/')) {
403              const newUrl = window.getApiUrl(url);
404              console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "→", newUrl);
405              return originalOpen.call(this, method, newUrl, ...rest);
406            }
407            
408            // Case 2: Handle other relative paths
409            else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
410              const newUrl = window.getPreviewUrl(url);
411              console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "→", newUrl);
412              return originalOpen.call(this, method, newUrl, ...rest);
413            }
414            
415            // Case 3: Handle absolute URLs to the same domain containing /api/
416            else if (url.includes(window.location.host) && url.includes('/api/')) {
417              try {
418                const urlObj = new URL(url);
419                if (urlObj.hostname === window.location.hostname) {
420                  const path = urlObj.pathname;
421                  if (path.startsWith('/' + previewGuid + '/')) {
422                    return originalOpen.call(this, method, url, ...rest);
423                  }
424                  const newPath = '/' + previewGuid + path;
425                  urlObj.pathname = newPath;
426                  const newUrl = urlObj.toString();
427                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "→", newUrl);
428                  return originalOpen.call(this, method, newUrl, ...rest);
429                }
430              } catch (e) {
431                console.error("[Preview Helper] Error processing URL:", e);
432              }
433            }
434            
435            // Case 4: Handle absolute URLs to the same domain (non-API)
436            else if (url.includes(window.location.host)) {
437              try {
438                const urlObj = new URL(url);
439                if (urlObj.hostname === window.location.hostname && 
440                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
441                  const path = urlObj.pathname;
442                  const newPath = '/' + previewGuid + path;
443                  urlObj.pathname = newPath;
444                  const newUrl = urlObj.toString();
445                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "→", newUrl);
446                  return originalOpen.call(this, method, newUrl, ...rest);
447                }
448              } catch (e) {
449                console.error("[Preview Helper] Error processing URL:", e);
450              }
451            }
452          }
453          return originalOpen.call(this, method, url, ...rest);
454        };
455        console.log("[Preview Helper] Patched XMLHttpRequest");
456        
457        // Note: Link click handling is done by the existing gallery link interceptor
458        // The fetch and XMLHttpRequest patches above handle programmatic requests
459        
460        // Add debugging aid
461        window.debugPreviewUrls = function() {
462          return {
463            hostname: window.location.hostname,
464            pathname: window.location.pathname,
465            guid: window.previewGuid,
466            isPreviewMode: window.isPreviewMode,
467            sampleTransformations: {
468              relativeApi: window.getApiUrl('/api/menu-styles'),
469              relativeLink: window.getPreviewUrl('/gallery'),
470              absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')
471            }
472          };
473        };
474        
475        // Test the transformation immediately
476        console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles'));
477        console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery'));
478        console.log("[Preview Helper] Test absolute URL transformation:", 
479          new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/'));
480      }
481    })();
482  </script>
482
483  
484  
485  
486  
487<!-- Early CDN retry helpers: MUST run before CDN script/link tags.
488     Changing script.src after a load error is unreliable (esp. Firefox defer);
489     always insert a new element pointing at /cdn-proxy/. -->
490<script>
491(function() {
492  window.useCdnProxy = window.useCdnProxy || false;
493  window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || [];
494  function toProxy(url) {
495    if (!url || url.indexOf('cdn.neonsky.app') === -1) return url;
496    return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
497  }
498  function noteRetry(kind, fromUrl, toUrl) {
499    try {
500      window.useCdnProxy = true;
501      var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() };
502      window.__hydraCdnRetryLog.push(entry);
503      console.warn('🔄 CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl);
504    } catch (e) {}
505  }
506  window.__hydraRetryCdnScript = function(el) {
507    try {
508      if (!el || el.getAttribute('data-retried') === 'true') return;
509      var original = el.src || '';
510      if (original.indexOf('cdn.neonsky.app') === -1) return;
511      el.setAttribute('data-retried', 'true');
512      el.onerror = null;
513      var proxyUrl = toProxy(original);
514      noteRetry('script', original, proxyUrl);
515      var s = document.createElement('script');
516      s.src = proxyUrl;
517      s.setAttribute('data-retried', 'true');
518      if (el.id) s.id = el.id;
519      // Dynamic scripts ignore defer; omit it so the proxy copy executes on load.
520      if (el.parentNode) {
521        el.parentNode.insertBefore(s, el.nextSibling);
522      } else {
523        (document.head || document.documentElement).appendChild(s);
524      }
525    } catch (e) {}
526  };
527  window.__hydraRetryCdnLink = function(el) {
528    try {
529      if (!el || el.getAttribute('data-retried') === 'true') return;
530      var original = el.href || '';
531      if (original.indexOf('cdn.neonsky.app') === -1) return;
532      el.setAttribute('data-retried', 'true');
533      el.onerror = null;
534      var proxyUrl = toProxy(original);
535      noteRetry('link', original, proxyUrl);
536      var link = document.createElement('link');
537      link.rel = el.rel || 'stylesheet';
538      if (el.id) link.id = el.id;
539      link.href = proxyUrl;
540      link.setAttribute('data-retried', 'true');
541      if (el.parentNode) {
542        el.parentNode.insertBefore(link, el.nextSibling);
543      } else {
544        (document.head || document.documentElement).appendChild(link);
545      }
546    } catch (e) {}
547  };
548})();
549</script>
549
550<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1791322438466" onerror="window.__hydraRetryCdnLink(this)">
551<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 551
551
552<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 552
552
553<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 553
553
554<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 554
554
555<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
555
556<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1791322438466" onerror="window.__hydraRetryCdnLink(this)">
557<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 557
557
558<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 558
558
559<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 559
559
560<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 560
560
561<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 561
561
562<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 562
562
563<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 563
563
564<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
564
565<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
566<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1791322438466" onerror="window.__hydraRetryCdnLink(this)">
567
568
569
570<!-- CDN Fallback Handler for CSS and JS files -->
571<script>
572(function() {
573  // Global flag: Set to true when any CDN resource fails and uses proxy
574  window.useCdnProxy = window.useCdnProxy || false;
575  
576  // Global base URL function: Returns proxy base if flag is set, otherwise CDN base
577  window.getCdnBaseUrl = function() {
578    return window.useCdnProxy 
579      ? window.location.origin + '/cdn-proxy/'
580      : 'https://cdn.neonsky.app/';
581  };
582  
583  // Utility function to convert CDN URLs to proxy URLs
584  window.convertCdnToProxyUrl = function(cdnUrl) {
585    if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl;
586    return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
587  };
588  
589  // Utility function to construct CDN URLs (uses proxy if flag is set)
590  window.buildCdnUrl = function(path) {
591    return window.getCdnBaseUrl() + path;
592  };
593  
594  // Resource tracking for debugging
595  window._resourceTracking = window._resourceTracking || {
596    resources: [],
597    proxyFlagChanges: [],
598    imageLoads: [],
599    networkErrors: []
600  };
601  
602  // Track when proxy flag changes - use a wrapper function
603  let proxyFlagChangeCount = 0;
604  let _useCdnProxyValue = window.useCdnProxy || false;
605  
606  // Create a setter function that tracks changes
607  window._setUseCdnProxy = function(newValue, reason) {
608    const oldValue = _useCdnProxyValue;
609    if (oldValue !== newValue) {
610      proxyFlagChangeCount++;
611      const changeInfo = {
612        timestamp: new Date().toISOString(),
613        changeNumber: proxyFlagChangeCount,
614        oldValue: oldValue,
615        newValue: newValue,
616        reason: reason || 'Unknown',
617        stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack'
618      };
619      window._resourceTracking.proxyFlagChanges.push(changeInfo);
620      console.error('🔍 PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2));
621      // Keep only last 50 changes
622      if (window._resourceTracking.proxyFlagChanges.length > 50) {
623        window._resourceTracking.proxyFlagChanges.shift();
624      }
625      _useCdnProxyValue = newValue;
626      window.useCdnProxy = newValue;
627    }
628  };
629  
630  // Track resource loading
631  function trackResource(url, type, status, details) {
632    const entry = {
633      timestamp: new Date().toISOString(),
634      url: url,
635      type: type, // 'script', 'link', 'image', etc.
636      status: status, // 'loading', 'success', 'failed', 'proxy', 'storage'
637      details: details || {},
638      useCdnProxy: window.useCdnProxy
639    };
640    window._resourceTracking.resources.push(entry);
641    console.error('📊 RESOURCE_TRACK:', JSON.stringify(entry, null, 2));
642    // Keep only last 200 resources
643    if (window._resourceTracking.resources.length > 200) {
644      window._resourceTracking.resources.shift();
645    }
646  }
647  
648  // Track image loads
649  function trackImageLoad(url, status, source) {
650    const entry = {
651      timestamp: new Date().toISOString(),
652      url: url,
653      status: status, // 'loading', 'success', 'failed'
654      source: source, // 'cdn', 'proxy', 'storage'
655      useCdnProxy: window.useCdnProxy
656    };
657    window._resourceTracking.imageLoads.push(entry);
658    console.error('🖼️ IMAGE_LOAD:', JSON.stringify(entry, null, 2));
659    // Keep only last 100 image loads
660    if (window._resourceTracking.imageLoads.length > 100) {
661      window._resourceTracking.imageLoads.shift();
662    }
663  }
664  
665  // Track network errors
666  function trackNetworkError(url, error, context) {
667    const entry = {
668      timestamp: new Date().toISOString(),
669      url: url,
670      error: {
671        message: error?.message || 'Unknown error',
672        name: error?.name || 'Unknown',
673        type: error?.type || 'unknown',
674        toString: String(error)
675      },
676      context: context || {},
677      useCdnProxy: window.useCdnProxy
678    };
679    window._resourceTracking.networkErrors.push(entry);
680    console.error('❌ NETWORK_ERROR:', JSON.stringify(entry, null, 2));
681    // Keep only last 100 errors
682    if (window._resourceTracking.networkErrors.length > 100) {
683      window._resourceTracking.networkErrors.shift();
684    }
685  }
686  
687  // Expose tracking functions globally
688  window.trackResource = trackResource;
689  window.trackImageLoad = trackImageLoad;
690  window.trackNetworkError = trackNetworkError;
691  
692  // Function to retry failed resources via proxy
693  // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag
694  // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it
695  function retryViaProxy(element, originalSrc, errorEvent) {
696    if (!originalSrc) originalSrc = element.href || element.src;
697    if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return;
698    
699    // Prevent multiple retries
700    if (element.getAttribute('data-retried')) {
701      return;
702    }
703    element.setAttribute('data-retried', 'true');
704    
705    // Check if element still exists in DOM
706    if (!element.parentNode) {
707      console.warn('⚠️ Element no longer in DOM, skipping proxy retry:', originalSrc);
708      return;
709    }
710    
711    // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s
712    // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy
713    // Check error message if available, but default to assuming network error
714    const hasExplicitNetworkError = errorEvent && errorEvent.message && (
715      errorEvent.message.includes('Failed to fetch') ||
716      errorEvent.message.includes('SSL') ||
717      errorEvent.message.includes('secure connection') ||
718      errorEvent.message.includes('network') ||
719      errorEvent.message.includes('ERR_') ||
720      errorEvent.message.includes('SSL_ERROR')
721    );
722    
723    // Track the error
724    trackNetworkError(originalSrc, errorEvent, {
725      elementType: element.tagName,
726      hasExplicitNetworkError: hasExplicitNetworkError,
727      errorMessage: errorEvent?.message || 'No error message',
728      errorName: errorEvent?.name || 'No error name'
729    });
730    
731    // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure)
732    // This covers ISP-affected clients where SSL handshake fails before HTTP request
733    const reason = hasExplicitNetworkError 
734      ? 'Explicit network/SSL error detected'
735      : (!errorEvent || !errorEvent.message)
736        ? 'Browser silent failure (likely SSL/network)'
737        : 'CDN resource failed (using proxy as fallback)';
738    
739    if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) {
740      window._setUseCdnProxy(true, reason);
741      console.error('🚩 CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests');
742      console.error('   Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event');
743    } else {
744      // Even for other errors, set the flag to be safe - proxy can handle 404s too
745      window._setUseCdnProxy(true, reason);
746      console.error('🚩 CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback');
747    }
748    
749    const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
750    console.error('🔄 CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl);
751    
752    // Track the retry
753    trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', {
754      retryUrl: proxyUrl,
755      error: errorEvent?.message || 'Unknown'
756    });
757    
758    if (element.tagName === 'SCRIPT') {
759      const newScript = document.createElement('script');
760      newScript.src = proxyUrl;
761      // Do not copy defer/async onto dynamically inserted scripts. After a CDN
762      // failure, DOMContentLoaded may already have fired; deferred dynamic
763      // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients).
764      newScript.setAttribute('data-retried', 'true');
765      newScript.onload = function() {
766        trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc });
767      };
768      newScript.onerror = function() {
769        trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc });
770        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
771        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
772        trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
773        newScript.src = storageUrl;
774        newScript.onload = function() {
775          trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc });
776        };
777      };
778      element.parentNode.replaceChild(newScript, element);
779    } else if (element.tagName === 'LINK') {
780      const newLink = document.createElement('link');
781      newLink.rel = element.rel;
782      newLink.href = proxyUrl;
783      newLink.setAttribute('data-retried', 'true');
784      newLink.onload = function() {
785        trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc });
786      };
787      newLink.onerror = function() {
788        trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc });
789        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
790        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
791        trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
792        newLink.href = storageUrl;
793        newLink.onload = function() {
794          trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc });
795        };
796      };
797      element.parentNode.replaceChild(newLink, element);
798    }
799  }
800  
801  // Monitor script and link tags for errors
802  document.addEventListener('error', function(e) {
803    const target = e.target;
804    if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 
805        target.src && target.src.includes('cdn.neonsky.app')) {
806      retryViaProxy(target, target.src, e);
807    } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) {
808      retryViaProxy(target, target.href, e);
809    }
810  }, true);
811  
812  // Also check existing scripts/links that may have failed before this script loaded
813  // For ISP-affected clients, we assume CDN failures are network/SSL errors
814  setTimeout(function() {
815    document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) {
816      if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
817        el.addEventListener('error', function(errorEvent) {
818          // For ISP-affected clients, assume any CDN failure is a network/SSL error
819          // The proxy can handle both network errors and 404s
820          retryViaProxy(el, el.src, errorEvent);
821        });
822      } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
823        // CSS link failed if it has no stylesheet - check after a delay
824        setTimeout(function() {
825          if (!el.sheet && !el.getAttribute('data-retried')) {
826            // For ISP-affected clients, assume any CDN failure is a network/SSL error
827            retryViaProxy(el, el.href, null);
828          }
829        }, 2000);
830      }
831    });
832  }, 100);
833})();
834</script>
834
835
836<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
837<script>
838(function() {
839  'use strict';
840  
841  // Only initialize if not already initialized
842  if (window._indexLogCaptureInitialized) {
843    return;
844  }
845  window._indexLogCaptureInitialized = true;
846  
847  // Wait for DOM to be ready
848  function initLogCapture() {
849    // Store original console methods
850    const originalConsole = {
851      log: console.log,
852      warn: console.warn,
853      error: console.error,
854      info: console.info
855    };
856
857    // Array to store all console messages
858    window._capturedLogs = window._capturedLogs || [];
859
860    // Override console methods to capture logs
861    ['log', 'warn', 'error', 'info'].forEach(method => {
862      console[method] = function(...args) {
863        // Call original method
864        originalConsole[method].apply(console, args);
865        
866        // Capture the log with timestamp
867        window._capturedLogs.push({
868          timestamp: new Date().toISOString(),
869          level: method.toUpperCase(),
870          message: args.map(function(arg) {
871            if (arg !== null && arg !== undefined && Object(arg) === arg) {
872              try {
873                return JSON.stringify(arg, null, 2);
874              } catch (e) {
875                return String(arg);
876              }
877            }
878            return String(arg);
879          }).join(' ')
880        });
881        
882        // Keep only last 2000 logs to prevent memory issues
883        if (window._capturedLogs.length > 2000) {
884          window._capturedLogs.shift();
885        }
886      };
887    });
888
889    // Add keyboard shortcut to download logs (S+J)
890    let pressedKeys = new Set();
891    
892    document.addEventListener('keydown', function(event) {
893      const key = (event.key || '').toLowerCase();
894      
895      if (key === 's' || key === 'j') {
896        pressedKeys.add(key);
897        
898        // Check if both S and J are pressed
899        if (pressedKeys.has('s') && pressedKeys.has('j')) {
900          event.preventDefault();
901          console.log('S+J detected - downloading logs...');
902          
903          // Show a brief visual indicator
904          const indicator = document.createElement('div');
905          indicator.style.cssText = 
906            'position: fixed; top: 20px; right: 20px; z-index: 10000; ' +
907            'background: #4CAF50; color: white; padding: 10px 20px; ' +
908            'border-radius: 5px; font-family: Arial, sans-serif; ' +
909            'font-size: 14px; font-weight: bold; ' +
910            'box-shadow: 0 2px 10px rgba(0,0,0,0.3);';
911          indicator.textContent = '📥 Downloading logs...';
912          document.body.appendChild(indicator);
913          
914          setTimeout(() => {
915            if (indicator.parentNode) {
916              indicator.parentNode.removeChild(indicator);
917            }
918          }, 2000);
919          
920          downloadLogs();
921          // Clear the set after triggering
922          pressedKeys.clear();
923        }
924      }
925    });
926    
927    document.addEventListener('keyup', function(event) {
928      const key = (event.key || '').toLowerCase();
929      if (key === 's' || key === 'j') {
930        pressedKeys.delete(key);
931      }
932    });
933
934    console.log('📋 Log capture system initialized. Press S+J simultaneously to download logs.');
935  }
936
937  // Download captured logs as a text file
938  function downloadLogs() {
939    try {
940      // Get all scripts and links, categorized
941      const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
942      const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href);
943      
944      // Get additional debug info
945      const debugInfo = {
946        url: window.location.href,
947        userAgent: navigator.userAgent,
948        timestamp: new Date().toISOString(),
949        useCdnProxy: window.useCdnProxy || false,
950        windowLocation: window.location.toString(),
951        documentReadyState: document.readyState,
952        // All scripts/links (not just CDN)
953        allScriptsLoaded: allScripts,
954        allLinksLoaded: allLinks,
955        // CDN-specific (for backward compatibility and CDN debugging)
956        scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')),
957        linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')),
958        // Gallery scripts (loaded from Worker)
959        galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')),
960        galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')),
961        // Failed resources
962        failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src),
963        failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href),
964        // Proxy resources
965        proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')),
966        proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')),
967        // Resource tracking data
968        resourceTracking: window._resourceTracking || {
969          resources: [],
970          proxyFlagChanges: [],
971          imageLoads: [],
972          networkErrors: []
973        },
974        // Performance timing
975        performanceTiming: performance.timing ? {
976          navigationStart: performance.timing.navigationStart,
977          domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
978          loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
979          domInteractive: performance.timing.domInteractive - performance.timing.navigationStart
980        } : null,
981        // Style / logo diagnostics (layout/color bug triage)
982        styleDiagnostics: (function() {
983          try {
984            var rootStyle = getComputedStyle(document.documentElement);
985            var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img'));
986            return {
987              siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(),
988              menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(),
989              menuColor: rootStyle.getPropertyValue('--menu-color').trim(),
990              inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'),
991              inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'),
992              hasMenuStyleCustomizer: !!window.MenuStyleCustomizer,
993              menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized),
994              menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
995                ? {
996                    siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor,
997                    menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor,
998                    color: window.MenuStyleCustomizer.settings.color
999                  }
1000                : null,
1001              hasSidebarManager: !!window.SidebarManager,
1002              sidebarElementCount: window.SidebarManager && window.SidebarManager.elements
1003                ? window.SidebarManager.elements.length
1004                : 0,
1005              hasPageManager: !!window.PageManager,
1006              pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'),
1007              pageElementCount: (function() {
1008                try {
1009                  return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length;
1010                } catch (e) { return -1; }
1011              })(),
1012              pageContainerHtmlLength: (function() {
1013                try {
1014                  var pc = document.querySelector('.gallery-container .page-container, .page-container');
1015                  return pc ? (pc.innerHTML || '').length : 0;
1016                } catch (e) { return -1; }
1017              })(),
1018              activeContentType: window._activeContentType || null,
1019              logoImageCount: logoNodes.length,
1020              logoImages: logoNodes.slice(0, 5).map(function(img) {
1021                return {
1022                  src: img.currentSrc || img.src || '',
1023                  naturalWidth: img.naturalWidth || 0,
1024                  display: getComputedStyle(img).display,
1025                  complete: !!img.complete
1026                };
1027              }),
1028              earlyCdnRetries: window.__hydraCdnRetryLog || []
1029            };
1030          } catch (e) {
1031            return { error: String(e && e.message ? e.message : e) };
1032          }
1033        })()
1034      };
1035
1036      // Create log content
1037      const logContent = [
1038        '=== INDEX.TS DEBUG LOGS ===',
1039        'Generated: ' + debugInfo.timestamp,
1040        'URL: ' + debugInfo.url,
1041        'User Agent: ' + debugInfo.userAgent,
1042        'useCdnProxy Flag: ' + debugInfo.useCdnProxy,
1043        'Document Ready State: ' + debugInfo.documentReadyState,
1044        '================================',
1045        'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.',
1046        'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).',
1047        '================================',
1048        '',
1049        '=== DEBUG INFO ===',
1050        JSON.stringify(debugInfo, null, 2),
1051        '',
1052        '=== STYLE DIAGNOSTICS ===',
1053        JSON.stringify(debugInfo.styleDiagnostics, null, 2),
1054        '',
1055        '=== RESOURCE TRACKING SUMMARY ===',
1056        'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0),
1057        'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0),
1058        'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0),
1059        'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0),
1060        '',
1061        '=== PROXY FLAG CHANGES ===',
1062        JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2),
1063        '',
1064        '=== RESOURCE LOADING TRACKING ===',
1065        JSON.stringify(debugInfo.resourceTracking.resources, null, 2),
1066        '',
1067        '=== IMAGE LOADING TRACKING ===',
1068        JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2),
1069        '',
1070        '=== NETWORK ERRORS ===',
1071        JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2),
1072        '',
1073        '=== CONSOLE LOGS ===',
1074        ...(window._capturedLogs || []).map(log => 
1075          '[' + log.timestamp + '] ' + log.level + ': ' + log.message
1076        )
1077      ].join('\n');
1078
1079      // Create and download file
1080      const blob = new Blob([logContent], { type: 'text/plain' });
1081      const url = URL.createObjectURL(blob);
1082      const a = document.createElement('a');
1083      a.href = url;
1084      a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
1085      document.body.appendChild(a);
1086      a.click();
1087      document.body.removeChild(a);
1088      URL.revokeObjectURL(url);
1089
1090      console.log('✅ Debug logs downloaded successfully!');
1091    } catch (error) {
1092      console.error('❌ Failed to download logs:', error);
1093      alert('Failed to download logs. Error: ' + (error.message || 'Unknown error'));
1094    }
1095  }
1096
1097  // Initialize when DOM is ready
1098  if (document.readyState === 'loading') {
1099    document.addEventListener('DOMContentLoaded', initLogCapture);
1100  } else {
1101    // DOM already ready, initialize immediately
1102    setTimeout(initLogCapture, 100);
1103  }
1104})();
1105</script>
1105
1106
1107<!-- Add auth token if available -->
1108
1109
1110<style>
1111/* Override Pico.css body padding to prevent jumping between pages */
1112body {
1113  padding: 0 !important;
1114  margin: 0 !important;
1115  display: block !important;
1116}
1117
1118/* Scope Pico.css to only affect elements in edit mode */
1119:root:not([data-theme="pico"]) {
1120--pico-applied: dark;
1121}
1122
1123/* Add data-theme="pico" to body when in edit mode */
1124body.edit-mode-active {
1125data-theme: dark;
1126}
1127
1128/* Custom styles to ensure Pico only affects editor UI, not content */
1129.sidebar.editing button,
1130.edit-controls button,
1131.element-header button,
1132.text-editor button,
1133.image-uploader button {
1134--pico-applied: dark;
1135}
1136
1137/* Hide default browser-styling when Pico is active */
1138[data-theme="pico"] button.btn {
1139appearance: dark;
1140-webkit-appearance: dark;
1141}
1142
1143/* Ensure content isn't affected by Pico styles */
1144.gallery-container,
1145.page-text,
1146.sidebar-text {
1147--pico-applied: none !important;
1148}
1149
1150/* Conflict Indicator Styles */
1151#conflictIndicator {
1152  background: #d32f2f !important;
1153  border: 1px solid #d32f2f !important;
1154  border-radius: 4px;
1155  padding: 8px;
1156  cursor: pointer;
1157  transition: opacity 0.2s;
1158  margin-left: 8px;
1159  display: flex; /* Use flex for centering, but hidden by default via inline style */
1160  align-items: center;
1161  justify-content: center;
1162}
1163
1164#conflictIndicator:hover {
1165  background: #e53935 !important;
1166  border-color: #e53935 !important;
1167  opacity: 0.9;
1168}
1169
1170#conflictIndicator svg {
1171  display: block;
1172  width: 24px;
1173  height: 24px;
1174  margin: 0 auto;
1175  color: white !important;
1176}
1177
1178/* Tooltip styles for conflict indicator - allow wrapping */
1179#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"],
1180#tooltip-container .tooltip {
1181  max-width: 200px;
1182  word-wrap: break-word;
1183  white-space: normal;
1184  line-height: 1.4;
1185}
1186
1187.global-header-controls {
1188  display: flex;
1189  align-items: center;
1190  gap: 8px;
1191}
1192
1193.sidebar-header {
1194  display: flex;
1195  align-items: center;
1196  gap: 8px;
1197}
1198
1199/* Cookie Notice — paper UI (public-safe; does not depend on edit-mode pe-* scopes) */
1200.cookie-notice {
1201  --cn-paper: #f6f5f1;
1202  --cn-paper-elevated: #fdfcfa;
1203  --cn-ink: #2c2a27;
1204  --cn-ink-muted: #6b6760;
1205  --cn-ink-faint: #9b968d;
1206  --cn-rule: rgba(44, 42, 39, 0.1);
1207  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1208  --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1);
1209  --cn-radius: 3px;
1210  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1211
1212  position: fixed;
1213  bottom: 0;
1214  left: 0;
1215  right: 0;
1216  z-index: 10000;
1217  display: flex;
1218  align-items: center;
1219  justify-content: space-between;
1220  flex-wrap: wrap;
1221  gap: 0.85rem 1.25rem;
1222  padding: 1.1rem 1.35rem 1.2rem;
1223  font-family: var(--cn-font);
1224  font-size: 14px;
1225  line-height: 1.45;
1226  color: var(--cn-ink);
1227  background: var(--cn-paper-elevated);
1228  border-top: 1px solid var(--cn-rule);
1229  box-shadow: var(--cn-shadow);
1230}
1231
1232.cookie-notice.hidden {
1233  display: none;
1234}
1235
1236.cookie-notice-text {
1237  flex: 1 1 220px;
1238  min-width: 0;
1239  margin: 0;
1240  color: var(--cn-ink-muted);
1241  font-size: 13px;
1242}
1243
1244.cookie-notice-text a.cookie-notice-policy {
1245  color: var(--cn-ink);
1246  font-weight: 600;
1247  text-decoration: underline;
1248  text-underline-offset: 2px;
1249}
1250
1251.cookie-notice-text a.cookie-notice-policy:hover {
1252  color: var(--cn-ink-muted);
1253}
1254
1255.cookie-notice-buttons {
1256  display: flex;
1257  flex-wrap: wrap;
1258  align-items: center;
1259  justify-content: flex-end;
1260  gap: 0.5rem;
1261}
1262
1263.cookie-notice .pe-btn {
1264  display: inline-flex;
1265  align-items: center;
1266  justify-content: center;
1267  gap: 0.4rem;
1268  margin: 0;
1269  padding: 0.45rem 0.85rem;
1270  font-family: var(--cn-font);
1271  font-size: 11px;
1272  font-weight: 600;
1273  letter-spacing: 0.1em;
1274  text-transform: uppercase;
1275  color: var(--cn-ink-muted);
1276  background: var(--cn-paper);
1277  border: 1px solid var(--cn-rule-strong);
1278  border-radius: var(--cn-radius);
1279  box-shadow: none;
1280  cursor: pointer;
1281  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1282}
1283
1284.cookie-notice .pe-btn:hover {
1285  color: var(--cn-ink);
1286  background: var(--cn-paper-elevated);
1287  border-color: rgba(44, 42, 39, 0.22);
1288}
1289
1290.cookie-notice .pe-btn--primary {
1291  color: var(--cn-ink);
1292  background: var(--cn-paper);
1293  border-color: var(--cn-rule-strong);
1294}
1295
1296.cookie-notice .pe-btn--primary:hover {
1297  color: var(--cn-ink);
1298  background: var(--cn-paper-elevated);
1299  border-color: rgba(44, 42, 39, 0.22);
1300}
1301
1302#hydraDoNotSellLink {
1303  --cn-paper: #f6f5f1;
1304  --cn-paper-elevated: #fdfcfa;
1305  --cn-ink: #2c2a27;
1306  --cn-ink-muted: #6b6760;
1307  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1308  --cn-radius: 3px;
1309  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1310
1311  position: fixed;
1312  bottom: 10px;
1313  left: 12px;
1314  z-index: 9999;
1315  display: inline-flex;
1316  align-items: center;
1317  justify-content: center;
1318  margin: 0;
1319  padding: 0.45rem 0.85rem;
1320  font-family: var(--cn-font);
1321  font-size: 11px;
1322  font-weight: 600;
1323  letter-spacing: 0.1em;
1324  text-transform: uppercase;
1325  color: var(--cn-ink-muted);
1326  background: var(--cn-paper-elevated);
1327  border: 1px solid var(--cn-rule-strong);
1328  border-radius: var(--cn-radius);
1329  box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06);
1330  cursor: pointer;
1331}
1332
1333#hydraDoNotSellLink.hidden {
1334  display: none;
1335}
1336
1337#hydraDoNotSellLink:hover {
1338  color: var(--cn-ink);
1339  background: var(--cn-paper);
1340  border-color: rgba(44, 42, 39, 0.22);
1341}
1342
1343/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */
1344html.hydra-ready {
1345  opacity: 1;
1346  transition: opacity 0.2s ease;
1347}
1348/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */
1349body.hydra-ready {
1350  visibility: visible;
1351}
1352
1353/* Admin panel typography */
1354.sidebar-element-form,
1355.sidebar-element-form *,
1356.add-form,
1357.add-form *,
1358.metadata-editor,
1359.metadata-editor * {
1360  font-family: "Hanken Grotesk", sans-serif;
1361  font-weight: 200;
1362  letter-spacing: 0.02em;
1363}
1364
1365.sidebar-element-form h3,
1366.sidebar-element-form label,
1367.add-form label,
1368.metadata-editor h3,
1369.metadata-editor label {
1370  text-transform: uppercase;
1371  letter-spacing: 0.04em;
1372  font-weight: 300;
1373}
1374
1375.sidebar-element-form .form-actions .btn,
1376.sidebar-element-form button,
1377.add-form .form-actions .btn,
1378.add-form button,
1379.metadata-editor .form-actions .btn,
1380.metadata-editor button {
1381  text-transform: uppercase;
1382  letter-spacing: 0.08em;
1383  font-weight: 300;
1384}
1385
1386.sidebar-element-form input,
1387.sidebar-element-form select,
1388.sidebar-element-form textarea,
1389.sidebar-element-form button,
1390.add-form input,
1391.add-form select,
1392.add-form textarea,
1393.add-form button,
1394.metadata-editor input,
1395.metadata-editor select,
1396.metadata-editor textarea,
1397.metadata-editor button {
1398  font-family: "Hanken Grotesk", sans-serif;
1399  font-weight: 200;
1400  letter-spacing: 0.02em;
1401  border-radius: 0;
1402}
1403</style>
1404
1405<script>
1406// Flag to prevent duplicate initialization
1407window.hydraInitialized = false;
1408
1409function ensureAdminPanelFont() {
1410  const head = document.head || document.getElementsByTagName('head')[0];
1411  if (!head) {
1412    return;
1413  }
1414
1415  const linkDefinitions = [
1416    {
1417      id: 'admin-panels-font-preconnect-googleapis',
1418      rel: 'preconnect',
1419      href: 'https://fonts.googleapis.com'
1420    },
1421    {
1422      id: 'admin-panels-font-preconnect-gstatic',
1423      rel: 'preconnect',
1424      href: 'https://fonts.gstatic.com',
1425      crossOrigin: 'anonymous'
1426    },
1427    {
1428      id: 'admin-panels-font-stylesheet',
1429      rel: 'stylesheet',
1430      href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap'
1431    },
1432    {
1433      id: 'admin-panels-font-barlow',
1434      rel: 'stylesheet',
1435      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'
1436    }
1437  ];
1438
1439  for (const definition of linkDefinitions) {
1440    if (!document.getElementById(definition.id)) {
1441      const link = document.createElement('link');
1442      link.id = definition.id;
1443      link.rel = definition.rel;
1444      link.href = definition.href;
1445
1446      if (definition.crossOrigin) {
1447        link.crossOrigin = definition.crossOrigin;
1448      }
1449
1450      head.appendChild(link);
1451    }
1452  }
1453}
1454
1455ensureAdminPanelFont();
1456
1457 window.Parameters = window.Parameters || {};
1458  window.Parameters.siteId = "q7u17cy4";
1459  window.Parameters.SiteId = "q7u17cy4"; // Also set capitalized version
1460  window.siteId = "q7u17cy4";
1461  window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp";
1462  window.NEON_SKY_FILES = window.NEON_SKY_FILES || {};
1463  window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js';
1464  window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css';
1465  window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json';
1466
1467// Initialize menu styles from server
1468document.addEventListener('DOMContentLoaded', function() {
1469// Apply initial menu styles from server
1470const initialMenuStyles = {"fontFamily":"Outfit, sans-serif","fontSize":"14px","menuTextUppercase":false,"color":"#595959","activeColor":"#000000","hoverColor":"#000000","verticalSpacing":"2px","menuBackgroundColor":"#ffffff","menuBackgroundHidden":false,"siteBackgroundColor":"#ffffff","siteBackgroundUseGradient":false,"siteGradientRandomizeOnLoad":false,"artPaletteSelection":null,"menuBackgroundImageUrl":"","siteBackgroundImageUrl":"","mobileHeaderHeight":"60px","sidebarWidth":"19vw","sidebarPaddingTop":"20px","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","globalPagePaddingTopEnabled":true,"globalPagePaddingLeftEnabled":true,"globalPagePaddingRightEnabled":true,"globalPagePaddingBottomEnabled":false,"globalPagePaddingTop":"20vh","globalPagePaddingLeft":"10vw","globalPagePaddingRight":"10vw","globalPagePaddingBottom":"","globalGalleryPaddingTopEnabled":false,"globalGalleryPaddingLeftEnabled":false,"globalGalleryPaddingRightEnabled":false,"globalGalleryPaddingBottomEnabled":false,"globalGalleryPaddingTop":"20vh","globalGalleryPaddingLeft":"10vw","globalGalleryPaddingRight":"10vw","globalGalleryPaddingBottom":"","menuLayout":"sidebar","horizontalMenuAlignment":"left","horizontalHeaderLogoLayout":"inline","horizontalHeaderShadow":true,"disableRightClick":false,"showCookieNotice":false,"cookieNoticeText":"We use cookies and similar technologies (including analytics and optional scripts) to understand how you use this site. These run only if you select Accept. Select Decline to keep them off. You can also choose Do Not Sell or Share My Personal Information.","privacyPolicyUrl":"","alignElementsAtBottom":false,"customCSS":"","customJS":"","siteThemeId":null,"siteThemeOptions":{},"fontName":"Outfit"};
1471window.__hydraStyleFromQuery = null;
1472const initialSiteThemeRuntimeVars = null;
1473
1474// Set CSS custom properties
1475// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override).
1476if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) {
1477  Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) {
1478    document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]);
1479  });
1480  if (initialMenuStyles && initialMenuStyles.verticalSpacing) {
1481    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1482  }
1483  // Match menu chrome / Gallery Options overview preview (light weight)
1484  document.documentElement.style.setProperty('--menu-font-weight', '200');
1485  if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1486    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1487  }
1488} else if (initialMenuStyles) {
1489  if (initialMenuStyles.fontFamily) {
1490    document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily);
1491  }
1492  if (initialMenuStyles.fontSize) {
1493    document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize);
1494  }
1495  // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size
1496  if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1497    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1498  }
1499  document.documentElement.style.setProperty('--menu-font-weight', '200');
1500  if (initialMenuStyles.color) {
1501    document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color);
1502  }
1503  if (initialMenuStyles.activeColor) {
1504    document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor);
1505  }
1506  if (initialMenuStyles.hoverColor) {
1507    document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor);
1508  }
1509  // Background colors must be applied here too: MenuStyleCustomizer may load late/fail
1510  // after CDN→proxy retry, and CSS defaults are white/#fafafa.
1511  if (initialMenuStyles.siteBackgroundColor) {
1512    document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor);
1513  }
1514  if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) {
1515    document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor);
1516  }
1517  if (initialMenuStyles.verticalSpacing) {
1518    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1519  }
1520  if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') {
1521    var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim();
1522    document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')');
1523  }
1524  var resolvedSiteBg = '';
1525  if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') {
1526    resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true });
1527  } else if (initialMenuStyles.siteBackgroundImageUrl) {
1528    resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim();
1529  }
1530  if (resolvedSiteBg) {
1531    var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1;
1532    var siteBgImgCss = peIsCssGradient
1533      ? resolvedSiteBg
1534      : 'url(' + JSON.stringify(resolvedSiteBg) + ')';
1535    document.documentElement.style.setProperty('--site-background-image', siteBgImgCss);
1536    var siteBgSize = 'cover';
1537    if (peIsCssGradient) {
1538      siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroundSize
1539        ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg)
1540        : '100% 100%';
1541      document.body.classList.add('has-site-background-gradient');
1542    }
1543    document.documentElement.style.setProperty('--site-background-size', siteBgSize);
1544  }
1545  if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') {
1546    var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim();
1547    /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */
1548    var mhhParsed = parseInt(mhh0, 10);
1549    if (Number.isNaN(mhhParsed) || mhhParsed < 1) {
1550      mhh0 = '60px';
1551    }
1552    document.documentElement.style.setProperty('--mobile-header-height', mh
1552h0);
1553    document.documentElement.style.setProperty('--content-margin-top', mhh0);
1554    document.documentElement.style.setProperty('--top-bar-height-setting', mhh0);
1555  }
1556}
1557
1558if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) {
1559  document.body.classList.add('menu-background-hidden');
1560  document.documentElement.style.setProperty('--menu-background-color', 'transparent');
1561  document.documentElement.style.setProperty('--menu-background-image', 'none');
1562}
1563
1564if (initialMenuStyles && initialMenuStyles.menuTextUppercase) {
1565  document.body.classList.add('menu-text-uppercase');
1566  if (!document.getElementById('menu-text-uppercase-style')) {
1567    var menuUpperStyle = document.createElement('style');
1568    menuUpperStyle.id = 'menu-text-uppercase-style';
1569    menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}';
1570    document.head.appendChild(menuUpperStyle);
1571  }
1572}
1573
1574// Store initial styles in MenuStyleCustomizer if it exists
1575if (window.MenuStyleCustomizer) {
1576  window.MenuStyleCustomizer.settings = {
1577    ...window.MenuStyleCustomizer.defaults,
1578    ...initialMenuStyles
1579  };
1580}
1581});
1582</script>
vendor: 1 bytes, line 1582
1582
1583<script>
1584let magic = null;
1585let userMetadata = null;
1586let isAuthenticated = false;
1587let isAdmin = false;
1588let didToken = null;
1589let siteId = "q7u17cy4";
1590
1591/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */
1592function clearHydraClientAuthStorage() {
1593  try {
1594    localStorage.removeItem('hydra_is_admin');
1595    localStorage.removeItem('hydra_auth_token');
1596    localStorage.removeItem('hydra_auth_email');
1597    localStorage.removeItem('hydra_did_token');
1598    localStorage.removeItem('hydra_auth_site');
1599    localStorage.removeItem('hydra_auth_last_refresh');
1600    localStorage.removeItem('hydra_auth_auto_refresh');
1601  } catch (e) {}
1602  if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') {
1603    try { window.TokenManager.clearToken(); } catch (e) {}
1604  }
1605  isAuthenticated = false;
1606  isAdmin = false;
1607  didToken = null;
1608  userMetadata = null;
1609  window.isAuthenticated = false;
1610  window.isAdmin = false;
1611  window.didToken = null;
1612}
1613
1614function hydraUrlRequestsLogout() {
1615  try {
1616    var params = new URLSearchParams(window.location.search);
1617    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1618    if (params.get('logout') === '1' || params.get('logout') === 'true') return true;
1619    if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true;
1620    return path === '/_manage/logout';
1621  } catch (e) {
1622    return false;
1623  }
1624}
1625
1626function hydraCleanLogoutFromUrl() {
1627  try {
1628    var params = new URLSearchParams(window.location.search);
1629    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1630    var hadQuery = params.has('logout') || params.has('hydra_logout');
1631    params.delete('logout');
1632    params.delete('hydra_logout');
1633    var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname;
1634    var qs = params.toString();
1635    var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || '');
1636    if (hadQuery || path === '/_manage/logout') {
1637      history.replaceState({}, '', next);
1638    }
1639  } catch (e) {}
1640}
1641
1642(function hydraLogoutFromUrlEarly() {
1643  if (!hydraUrlRequestsLogout()) return;
1644  clearHydraClientAuthStorage();
1645  try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {}
1646  hydraCleanLogoutFromUrl();
1647  console.log('[Hydra] Auth cleared via logout link/query.');
1648})();
1649
1650function hydraUrlRequestsRefresh() {
1651  try {
1652    var params = new URLSearchParams(window.location.search);
1653    var v = params.get('refresh');
1654    return v === '1' || v === 'true';
1655  } catch (e) {
1656    return false;
1657  }
1658}
1659
1660function hydraClearGalleryClientCache() {
1661  try {
1662    Object.keys(localStorage).filter(function (key) {
1663      return key.includes('gallery') || key.includes('neon') || key.includes('lightbox');
1664    }
1664).forEach(function (key) {
1665      localStorage.removeItem(key);
1666    });
1667  } catch (e) {}
1668  try {
1669    delete window.__neonGalleryMainBootstrapWipeDone;
1670  } catch (e) {
1671    window.__neonGalleryMainBootstrapWipeDone = false;
1672  }
1673  window._forceGalleryReload = true;
1674}
1675
1676function hydraCleanRefreshFromUrl() {
1677  try {
1678    var params = new URLSearchParams(window.location.search);
1679    if (!params.has('refresh')) return;
1680    params.delete('refresh');
1681    var qs = params.toString();
1682    var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || '');
1683    history.replaceState({}, '', next);
1684  } catch (e) {}
1685}
1686
1687(function hydraRefreshFromUrlEarly() {
1688  if (!hydraUrlRequestsRefresh()) return;
1689  clearHydraClientAuthStorage();
1690  hydraClearGalleryClientCache();
1691  try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {}
1692  hydraCleanRefreshFromUrl();
1693  console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 — forcing fresh load.');
1694})();
1695
1696// State Management — site-wide KV metadata for the editor (not per-page SEO)
1697window.siteMetadata = {"title":"Eirini Vourloumis ","description":"Eirini Vourloumis is available for both photo assignments and freelance editing.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""};
1698console.log('Client Initialized site metadata:', window.siteMetadata);
1699
1700/** Match SSR title format: "{Gallery Title} | {Site Title}" */
1701function formatGalleryDocumentTitle(galleryTitle, win) {
1702  win = win || window;
1703  var meta = win.siteMetadata;
1704  var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname;
1705  return (galleryTitle || 'Gallery') + ' | ' + siteTitle;
1706}
1707
1708console.log('Client Google Analytics from metadataToUse:', '');
1709console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics);
1710console.log('Client Content Language:', window.siteMetadata.contentLanguage);
1711
1712// State Management
1713let galleries = [{"id":1786071153451,"title":"home","parentId":null,"visible":false,"isIntegrated":true,"pageId":"1it0jlwb","slug":"home","url":"/home","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","isIntegratedGallery":true,"pageId":"1it0jlwb","siteId":"q7u17cy4","siteAlias":"preview.neonsky.app","initialPageUuid":"1it0jlwb","initialPageAlias":"home","timestamp":1786071202336,"horizontalScrollerText":"<p><br></p>","startInSingles":true,"layoutSelection":"singles","useRowsLayout":false,"layoutType":"grid","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":2,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":true,"autoplayRandomStart":false,"autoplayDuration":"40","autoplayTransition":"1","showDescription":false,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":false,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"grid","selection":"singles","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"isPerma":false,"flyIoSaveTimestamp":1786071205387,"timestampsMatch":false,"allTimestampsMatch":false,"cdnNdjsonId":"8e6660c092d541a7b59b12ee21f580d1","cdnNdjsonUrl":"https://cdn.neonsky.app/8e6660c092d541a7b59b12ee21f580d1.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/q7u17cy4_1it0jlwb.json","cdnSaveTimestamp":1786071208016},"isHomePage":true,"siteId":"q7u17cy4","siteAlias":null,"isPage":false},{"id":1786071216950,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true},{"id":1786071223113,"title":"Spacer-copy","parentId":null,"visible":true,"isSpacer":true,"slug":"spacer-copy","url":"/spacer-copy","isHomePage":false,"position":2},{"id":1786070400574,"title":"In the Same Space","url":"/in-the-same-space","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"oh3tkrlx","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"in-the-same-space","normalizedName":"in-the-same-space","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0aa36d9","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"oh3tkrlx","horizontalScrollerText":"<p>In the Same Space, is a photographic dialogue with my grandfather’s Andreas Vourloumis (1910-199) paintings of Athens and the island of Aegina.&nbsp;My grandfather was part of the dynamic 1930’s generation of Greek artists. He was self-taught and his art was driven by capturing everyday life in the city. On his daily walks he worked very much like a documentary photographer, sketching buildings, pedestrians and daily interactions in the city. I used my mobile phone for this project as a way to mirror his own use of the sketch book in his daily routine. At the time I began this project I was working as a photojournalist covering the economic crisis for international media. I was motivated to begin this dialogue with my grandfather's paintings as a way to celebrate my hometown and it's people in a more positive way. The pairings are influenced by color, form, light and narrative context but mostly I was intent on reflecting the emotive state of my grandfather’s paintings. This series is a meditation on urban change, on spaces or places that have been lost, but also a tribute to my grandfather who never knew I became a photographer. In that sense, it is highly personal and emotional, and at the same time a social and anthropological take on a city that has undergone rapid transformation in the last fifty years.</p>","jsonCollections":{"GUID=4bd5ec0aa36d9":{"isClassicCollection":true,"guid":"GUID=4bd5ec0aa36d9","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"oh3tkrlx","initialPageAlias":"in-the-same-space","timestamp":1786082655213,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter"
1713:false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"isPerma":false},"isHomePage":false,"position":18,"siteAlias":null},{"id":1786070383350,"title":"In Waiting","isFolder":true,"isCollapsed":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"in-waiting","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":3},{"id":1786070383351,"title":"Book","url":"/book","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070383350,"siteId":"q7u17cy4","pageId":"cwd7vhoc","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"book","normalizedName":"book","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"cwd7vhoc","horizontalScrollerText":"<p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>In Waiting</b></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><strong style=\"font-size: 14px;\">Published by Hatje Cantz</strong></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><strong style=\"font-size: 14px;\">Release Date,October 2017</strong></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis</span></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">100 pg, 42 ills.</span></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">hardcover</span></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">9.00 x 24.00 cm</span></p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">SBN 978-3-7757-4340-2</span></p><p style=\"line-height: 1;\">\n</p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘In Waiting,’ is an examination of Athens and its role as a physical stage for the economic crisis. Primarily, I am exploring the interiors of government buildings, institutions, and schools to question how these spaces reflect modern Greek culture and character. The locations I chose are familiar microcosms for Athenians that have now become the public stage for a forced re-negotiation of Greek identity. I am attempting to personally rationalize how the deceptively tedious components of urban life factor into the economic crisis. I am specifically attracted to the particular aesthetic and surreal atmosphere I encounter in these spaces. These interiors also reveal an almost antiquated and bare aesthetic quality; they evoke in their décor a feeling of dependency on and idolization of the past. Spaces, which once seemed banal or unimportant, now reveal nuances of Greek reality and have social and political impl
1713ications. Like the Greek people, these places exist in anticipation of their future.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"cwd7vhoc","isHydra":true,"flyIoSaveTimestamp":1786070385097,"initialPageAlias":"book","timestamp":1786082268831,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"isPerma":false},"isHomePage":false,"position":5,"siteAlias":null},{"id":1786070385200,"title":"Selected images","url":"/selected-images","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070383350,"siteId":"q7u17cy4","pageId":"h0aj7770","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"selected-images","normalizedName":"selected-images","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"h0aj7770","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In Waiting,’ is an examination of Athens and its role as a physical stage for the economic crisis. Primarily, I am exploring the interiors of government buildings, institutions, and schools to question how these spaces reflect modern Greek culture and character. The locations I chose are familiar microcosms for Athenians that have now become the public stage for a forced re-negotiation of Greek identity. I am attempting to personally rationalize how the deceptively tedious components of urban life factor into the economic crisis. I am specifically attracted to the particular aesthetic and surreal atmosphere I encounter in these spaces. These interiors also reveal an almost antiquated and bare aesthetic quality; they evoke in their décor a feeling of dependency on and idolization of the past. Spaces, which once seemed banal or unimportant, now reveal nuances of Greek reality and have social and political impl
1713ications. Like the Greek people, these places exist in anticipation of their future.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":21}},"siteAlias":"preview.neonsky.app","initialPageUuid":"h0aj7770","isHydra":true,"flyIoSaveTimestamp":1786070387009,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":6},{"id":1786070387113,"title":"News","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070387113","visible":true,"parentId":1786070383350,"siteId":"q7u17cy4","slug":"news","url":"/news","pageElements":[{"id":1786070388013,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">March 25th, 2018</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"https://www.berlinerfestspiele.de/en/aktuell/festivals/gropiusbau/programm_mgb/gropius_bau_veranstaltungen/detail_gropius_bau_veranstaltungen_253977.php\" target=\"_blank\"><b>Martin Gropius Bau Museum, Berlin</b></a></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Solo exhibit, In Waiting</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In House with Wu Tsang</span></p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><br></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">January 2018</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"https://bookshop.thephotographersgallery.org.uk/blogs/events/eirini-vourloumis-in-waiting-talk-booksigning-thu-18-jan-18-30-20-00\" target=\"_blank\"><b>The Photographer’s Gallery, London</b></a></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘In Waiting’, screening and book launch'</span></p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><span class=\"ql-cursor\"></span></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">May 2nd-27th May 2018</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>Benaki Art Museum</b>, Athens</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘In Waiting’, Book exhibition</span></p>","textWidth":100,"fadeInOnScroll":false},{"id":1786082290513,"type":"metadata","visible":true,"position":1,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":7},{"id":1786070387114,"title":"Muslims In America","isFolder":true,"isCollapsed":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"muslims-in-america","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":8},{"id":1786070387116,"title":"Latino Muslims","url":"/latino-muslims","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070387114,"siteId":"q7u17cy4","pageId":"zymuxtc7","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"latino-muslims","normalizedName":"latino-muslims","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"zymuxtc7","horizontalScrollerText":"<p style=\"line-height: 1.5;\">There are a an estimated 200,000 Latinos who have converted to Islam in the United States the majority of whom live in New Jersey and New York City. A 2008 study by the Pew Forum on Religious and Public Life put the number of Latino Muslim U.S. residents at four percent of all Muslim U.S.residents.&nbsp;Even though this demographic of Latino Muslims is very small it introduces a new unique religious and cultural identity. The 44 million Latinos who live in the United States have always been closely associated with the Roman Catholic Church and more recently the evangelical Christian traditions.</p><p style=\"line-height: 1.5;\"><br></p><p style=\"line-height: 1.5;\">The most prominent Latino Muslim community in the East Coast is based out of the&nbsp;North Hudson Islamic Educational center in Union City, New Jersey (a predominantly Latino neighborhood), which counts 35 per cent of its congregation as Latino Muslims.&nbsp;The mosque has a very active Latino Dawah committee (volunteers who actively preach Islam) who conduct classes in Spanish teaching the Qu'ran and Islamic principals and who also organize an annual Latino Muslim day.&nbsp;Many conversions take place at this mosque where individuals recite the Shahadah, the Muslim declaration of belief in the oneness of Allah and acceptance of Muhammad as his prophet.</p><p style=\"line-height: 1.5;\"><br></p><p style=\"line-height: 1.5;\">Latinos convert to Islam for a variety of reasons, including disenchantment with the practices of Catholicism and the church establishment. These Latinos are lured by Islam's simplicity and the Muslim's independence of a mediating clergy in his or her relationship with God. Others believe they are reverting back to their true cultural traditions referring to the Moor influence (Spain's Islamic period) in Spain. Many converts today refer to themselves as 'reverts'. Others feel estranged from mainstream Christian America and feel that Muslims identify more closely to the Latino struggle for justice and equality.</p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"zymuxtc7","isHydra":true,"flyIoSaveTimestamp":1786070388847,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"initialPageAlias":"latino-muslims","timestamp":1786082464268,"filterMenuCollectionNames":"","filterMenuTitles":"","rolloverCollectionNames":"","isPerma":false},"isHomePage":false,"position":9,"siteAlias":null},{"id":1786070388954,"title":"Selected Assignments","isFolder":true,"isCollapsed":true,"visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"selected-assignments","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":10},{"id":1786070388955,"title":"Refugee Camps Crisis Greece-NYTimes","url":"/refugee-camps-crisis-greece-nytimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"ekwdjn33","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"refugee-camps-crisis-greece-nytimes","normalizedName":"refugee-camps-crisis-greece-nytimes","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"ekwdjn33","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":15}},"siteAlias":"preview.neonsky.app","initialPageUuid":"ekwdjn33","isHydra":true,"flyIoSaveTimestamp":1786070390685,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":11},{"id":1786070390794,"title":"Refugee Crisis Greece-Wall Street Journal","url":"/refugee-crisis-greece-wall-street-journal","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"scimz0hf","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"refugee-crisis-greece-wall-street-journal","normalizedName":"refugee-crisis-greece-wall-street-journal","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"scimz0hf","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":15}},"siteAlias":"preview.neonsky.app","initialPageUuid":"scimz0hf","isHydra":true,"flyIoSaveTimestamp":1786070392617,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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":"
1713center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":12},{"id":1786070392727,"title":"Soul Train-NYTimes","url":"/soul-train-nytimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"wyp3dqbg","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"soul-train-nytimes","normalizedName":"soul-train-nytimes","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"wyp3dqbg","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">These images are part of an assignment for the New York Tmes, accompaning author Roxana Robinson's piece,Soul Train. Her story explores her experiences of listening to subway musicians and the way in which the music brings an ambient and surreal experience to the regular routine of a subway commute.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The NYTimes slideshow can be viewed at this link.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://www.nytimes.com/slideshow/2009/05/17/nyregion/20090517-SUBWAY_index.html?scp=5&amp;sq=eirini%20vourloumis&amp;st=cse\" target=\"_blank\">http://www.nytimes.com/slideshow/2009/05/17/nyregion/20090517-SUBWAY_index.html?scp=5&amp;sq=eirini%20vourloumis&amp;st=cse</a></span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":11}},"siteAlias":"preview.neonsky.app","initialPageUuid":"wyp3dqbg","isHydra":true,"flyIoSaveTimestamp":1786070394417,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":13},{"id":1786070394528,"title":"Philly Fight Night-NYTimes","url":"/philly-fight-night-nytimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"lezuyzwg","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"philly-fight-night-nytimes","normalizedName":"philly-fight-night-nytimes","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"lezuyzwg","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The 2009 Philly Fight Night is organized by students of The Wharton School and Penn Law School of the University of Pennsylvania. This is a charity driven event to raise money for the Philadelphia Boys and Girls club. Students from both schools compete against eachother accompanied by entourages in WWF style costumes.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Part of this story was published as a photo essay in the  Sports section of the New York Times Sunday paper. Enter gallery below to see full story. NYTimes slideshow can be viewed at this link:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://www.nytimes.com/slideshow/2009/03/06/sports/20090306-boxing-slideshow_index.html?ref=sports\" target=\"_blank\">http://www.nytimes.com/slideshow/2009/03/06/sports/20090306-boxing-slideshow_index.html?ref=sports</a></span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":25}},"siteAlias":"preview.neonsky.app","initialPageUuid":"lezuyzwg","isHydra":true,"flyIoSaveTimestamp":1786070396772,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":14},{"id":1786070396884,"title":"The Decline of Perama Shipping Yards- The Financial Times","url":"/the-decline-of-perama-shipping-yards--the-financial-times","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"u8im8s66","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"the-decline-of-perama-shipping-yards--the-financial-times","normalizedName":"the-decline-of-perama-shipping-yards--the-financial-times","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"u8im8s66","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Perama repair docks known locally as \"the zone\" is one of the first victims of the ongoing economic crisis in Greece.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Perama was the most active repair zone for international shipping companies once called the Wall Street of Greece’s ship repair industry.Most repair companies have now left the area due to the decline of the number of ships coming in for repair due to continuous striking and union monopolizing. Shipping companies are sending their boats to repair yards in Asia, Romania, Italy and former Yugoslavia were repair costs are also substantially lower. Over recent years hundreds of</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">steel workers and mechanics have lost their jobs.  Since the start of the economic crisis Perama has suffered further losses and is in danger of closing entirely leaving hundreds of workers jobless.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":20}},"siteAlias":"preview.neonsky.app","initialPageUuid":"u8im8s66","isHydra":true,"flyIoSaveTimestamp":1786070398432,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter"
1713:false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":15},{"id":1786070398546,"title":"Centenarians in Ikaria-The Guardian","url":"/centenarians-in-ikaria-the-guardian","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"hye6o2kv","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"centenarians-in-ikaria-the-guardian","normalizedName":"centenarians-in-ikaria-the-guardian","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0aa36d9","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"hye6o2kv","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=4bd5ec0aa36d9":{"isClassicCollection":true,"guid":"GUID=4bd5ec0aa36d9","metadata":[],"totalItems":0}},"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":16},{"id":1786070398547,"title":"Syrian Refugees in Bulgaria-The New Statesman","url":"/syrian-refugees-in-bulgaria-the-new-statesman","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"979tezg7","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"syrian-refugees-in-bulgaria-the-new-statesman","normalizedName":"syrian-refugees-in-bulgaria-the-new-statesman","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"979tezg7","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":20}},"siteAlias":"preview.neonsky.app","initialPageUuid":"979tezg7","isHydra":true,"flyIoSaveTimestamp":1786070400459,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":17},{"id":1786070400575,"title":"The Mermaid Madonna","url":"/the-mermaid-madonna","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"9t7wfroe","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"the-mermaid-madonna","normalizedName":"the-mermaid-madonna","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0aa36d9","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"9t7wfroe","horizontalScrollerText":"<p>The Mermaid Madonna</p><p><br></p><p>Since February 2015, more than 850,000-recorded refugees have crossed over from Turkey onto the 15km stretch of coast on the northern side of the island of Lesbos, in the northeastern Aegean Sea in Greece. This project s titled after the book, The Mermaid Madonna (1955) by the acclaimed Lesbos born writer, Stratis Myrivilis.</p><p><br></p><p>The story takes place in the fishing village of Skala Sikaminias, one of the main entry points for refugees today. It is set in the 1920’s when this region witnessed its first wave of refugees during the forced population exchange between Greece with Turkey on the exact same coastline. At that time over 50,000 Greeks settled on Lesbos, many of which have remained on the north side of the island.</p><p><br></p><p>A majority of the village population today descend directly from refugees from the above mentioned population exchange. Due to the lack of imminent action from the Greek government and the European Union in 2015, the locals were the first to help refugees land safely and provide food and shelter. Their lives were severely disrupted as they were also forced to face the memories of their own family histories. This drove me to start a project driven stylistically by the magical realistic narrative of the text while examining the historical repetition of mass transitory movement on this coastline, and what role social memory plays in the permanence of landscape, questioning how physical environments may be continuous stages for different states of displacement.</p>","jsonCollections":{"GUID=4bd5ec0aa36d9":{"isClassicCollection":true,"guid":"GUID=4bd5ec0aa36d9","metadata":{},"totalItems":0}},"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter"
1713:false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"siteAlias":"preview.neonsky.app","initialPageUuid":"9t7wfroe","initialPageAlias":"the-mermaid-madonna","timestamp":1786082756750,"filterMenuCollectionNames":"","filterMenuTitles":"","rolloverCollectionNames":"","isPerma":false},"isHomePage":false,"position":19},{"id":1786070400576,"title":"Party Like There is No Tomorrow","url":"/party-like-there-is-no-tomorrow","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"b0q8nu8h","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"party-like-there-is-no-tomorrow","normalizedName":"party-like-there-is-no-tomorrow","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"b0q8nu8h","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">For my own personal coverage of the 2015 elections, my project Party Like there is no Tomorrow, I used the Fuji Intax camera and went to all the political rallies intending to reflect on the surreal atmosphere by producing a puzzle of triptychs. From the hotdog stands to the enamored supporters of both left and right I tried to present a collage of a classic political circus.  The election was for me an embodiment of a combination of the tragic and absurd which echoes our historic ancient Greek plays.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":8}},"siteAlias":"preview.neonsky.app","initialPageUuid":"b0q8nu8h","isHydra":true,"flyIoSaveTimestamp":1786070402767,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":20},{"id":1786070402885,"title":"Selected Tearsheets","url":"/selected-tearsheets","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"bmbthak2","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"selected-tearsheets","normalizedName":"selected-tearsheets","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"bmbthak2","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Tearsheets from International Heralad Tribune, Financial Times Magazine and The New York Times.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"bmbthak2","isHydra":true,"flyIoSaveTimestamp":1786070404871,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":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,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"grid","selection":"grid-columns","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"initialPageAlias":"selected-tearsheets","timestamp":1786082797768,"filterMenuCollectionNames":"","filterMenuTitles":"","rolloverCollectionNames":"","isPerma":false},"isHomePage":false,"position":21},{"id":1786070404990,"title":"Biography-Awards","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070404990","visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"biography-awards","url":"/biography-awards","pageElements":[{"id":1786070405891,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Eirini Vourloumis (b.1979) is a Greek- Indonesian visual artist, photographer and filmmaker based in Athens, Greece. Vourloumis is a graduate of Parsons School of Design and of the Columbia Graduate School of Journalism in New York. As a visual artist her photographic work has been exhibited in solo shows at EMST, The National Museum of Contemporary Art, the Benaki Museum and the Onassis Cultural Center in Athens. She has also participated in group shows at the Bozar Museum in Brussels, the Sharjah Biennial 19 in Sharjah and the Gropius Bau Museum in Berlin.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Vourloumis has also worked extensively as a photojournalist mostly covering the economic and refugee crises in Greece. Her reportage clients include The New York Times, The Wall Street Journal, The New Yorker, Time, Le Monde and The Guardian, among others. Vourloumis is currently directing her first feature length documentary, funded by ERT, Greek National Broadcaster, and the Greek Film Center.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Her recognitions include Ones to Watch, from the British Journal of Photography, and The New Yorker’s Emerging Photographers of the Year series. Her photographs have been acquired by EMST, and the Dakis Joannou, DESTE, Foundation for Contemporary Art collection. Her first book, In Waiting, was released by the German publisher Hatje Cantz. Vourloumis is a member of the Greek Depression Era collective.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>SOLO EXHIBIITIONS</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2023</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In the Same Space, The Municipal Art Gallery of Patras</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In collaboration with the National Contemporary   Museum of Art Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">A photographic dialogue with the paintings of  Andreas            Vourloumis curated by Stamatis Schizakis</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2022</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In the Same Space',National Contemporary    Museum of Art, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">A photographic dilaogue with the paintings of  Andreas Vourloumis curated by Stamatis Schizakis</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2019  </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Onassis Cultural Center, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Flash Forward Festival</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">
1713'The Mermaid Madonna'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In colloboration with Wu Tsang</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Screening: One Emerging From a Point of VIew</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2018 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Martin Gropius Bau Museum, Berlin</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In Waiting'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In House with Wu Tsang</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Benaki Art Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In Waiting’, Book exhibition</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The Photographer’s Gallery, London,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘In Waiting’, screening and book launch</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>GROUP EXHIBITIONS </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2022 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">National Contemporary Museum of Art, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Statecraft' curated by Katerina Gregos</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2019</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Benaki Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The DESTE Foundation and New Museum, New York</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'The Same River Twice' : Contemporary Art in Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Photo Espana Festival, Madrid</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Offland</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'The Mermaid Madonna'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Sharjah Bienniel 14</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Exhibit: 'The Mermaid Madonna'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In colloboration with Wu Tsang</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Screening: One Emerging From a Point of VIew</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Photography in Crisis', Depression Era, Slought Institute, Philadelphia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2018</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Photo-graphé', Benaki Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'The Tourists', Depression Era, Unseen Amsterdam</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2017         </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘The Decline of Heroes', Depression Era, Antikenmuseum, Basel,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2016     </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'On Elite Now', Kypseli Municipal Marketplace, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2015         </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Depression Era', Athens Biennale, Bagkeion Hotel, Athens,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Greek Stories Revisited', China Lishui Photography Festival, China</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Depression Era', Thessaloniki Biennale of Contemporary Art, Greece</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2014</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Depression Era', Hungarian Month of Photography, Budapest</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Depression Era', Le Mois de la Photo, Paris</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Depression Era', Benaki Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Lens Culture Emerging Talents’, Barcelona</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Tokyo International Photography Festival Competition 2014’,Tokyo,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">New York</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">‘Greek contemporary Art In Times of Crisis’</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Bozar Museum of Fine Art- Brussels, Belgium</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">1%: Privilege in a Time of Global Inequality</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Collection of photographers (one image)</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Published by Hatje Cantz edited by Myles Little</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Travelling show</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2018          </b>               Samek Art Museum, Lewisburg, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2017        </b>                 University of Virginia—Charlottesville Charlottesville, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">fosfeniLAB Padua, Italy</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Médiathèque du Pays de Sierentz Sierentz, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Photival Photographic Festival Wellington, New Zealand</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2016       </b>                  Addis Foto Fest, Addis Ababa, Ethiopia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Just Another Photo Festival, Varanasi, India</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Chavonnes Battery Museum, Cape Town, South Africa</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The Eye – International Photography Festival, Aberystwyth, Wales</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Organ Vida Festival Zagreb, Croatia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Brown University Watson Institute for International and Public Affairs</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Providence, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">AMURAL Festival Brasov, Romania</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">OBSCURA Festival George Town, Malaysia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Uri-Eichen Gallery, Chicago, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Voies Off :: Arles, Arles, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">LOOK3 Festival of the Photograph( projection) Charlottesville,,VA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Athens Photo Festival  Athens, Greece</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Transphotographiques Lilles, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Head On Photo Festival Sydney, Australia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Magazzini Fotografici Naples, Italy</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Gage Gallery at Roosevelt University Chicago, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Balkan Photo Festival Sarajevo, Bosnia and Herzegovina</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Rendez-vous  Image Strasbourg, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2015      </b>                International Photography Festival GuatePhoto, Guatemala City, Guatemal</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Lishui International Photography Festival, Lishui, China</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Lagos Photo (Satellite Exhibit) Lagos, Nigeria</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Fotodoks Festival (Satellite Exhibit), Munich, Germany</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Browse Gallery, Berlin, Germany</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">East Wing Gallery, Dubai, UAE</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Pingyao International Photography Festival, Pingyao, China</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>BOOKS </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">2017   In Waiting, Published by Hatje Cantz ( November)</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">42 illus. 100 pages. Essays by James Estrin and Panos Kokinnias</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">2015   1%: Privilege in a Time of Global Inequality</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Collection of photographers (one image)</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Published by Hatje Cantz edited by Myles Little</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>AWARDS RECOGNITION</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2015 </b><b>British Journal of Photography </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Ones to Watch Issue - 25 Emerging  Photographers</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>Tokyo International Photography Festival Competition </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Travelling exhibit of 8 winners     Tokyo, NY,  Berlin</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2014    </b><b>Duke Documentary Studies Competition</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Finalist</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>Lens Culture </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Emerging Talent</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>   PDN Annual </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Best imagery of 2014</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>  2013  </b><b>              Emerging Photographer series</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Photo booth-New Yorker</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2012   </b>                            American Photography- American Illustration</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Annual Photography Book</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b> 2006    </b>                           American Photography - American Illustration</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Annual Photography Book 2006</span></p>","textWidth":100}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":22},{"id":1786070404992,"title":"NYTimes Lens Blog Articles","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070404992","visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"nytimes-lens-blog-articles","url":"/nytimes-lens-blog-articles","pageElements":[{"id":1786070405892,"type":"text","t
1713itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Lens, the New York Times photojournalism blog showcases the finest work in photojournalism and documentary photography from established and emerging photographers. Below are the articles I have written for Lens presenting work from a variety of photographers.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Boogie:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"mailto:Paolo Woods:http://lens.blogs.nytimes.com/2009/12/11/showcase-91/http://lens.blogs.nytimes.com/2009/11/30/showcase-84/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/09/15/showcase-51/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Paolo Woods:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2009/12/11/showcase-91/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/12/11/showcase-91/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2009/11/30/showcase-84/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/11/30/showcase-84/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Alfred Yaghobzadeh:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2009/08/03/behind-9/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/08/03/behind-9/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Adam Ferguson:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2010/09/02/watching-a-teenage-girl-die-by-mortar/\" target=\"_blank\">http://lens.blogs.nytimes.com/2010/09/02/watching-a-teenage-girl-die-by-mortar/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Maciek Nabrdalik/VII  Agency Mentor series:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2010/04/01/showcase-146/\" target=\"_blank\">http://lens.blogs.nytimes.com/2010/04/01/showcase-146/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Agnes Dherbeys/VII  Agency Mentor series:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"mailto:http://lens.blogs.nytimes.com/2010/04/01/showcase-146/\" target=\"_blank\">http://lens</a><a href=\"http://lens.blogs.nytimes.com/2010/02/24/showcase-129/\" target=\"_blank\">.blogs.nytimes.com/2010/02/24/showcase-129/</a></span></p>","textWidth":100},{"id":1786071407160,"type":"metadata","visible":true,"position":1,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":23},{"id":1786070404993,"title":"Contact and current location","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070404993","visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"contact-and-current-location","url":"/contact-and-current-location","pageElements":[{"id":1786070405893,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Eirini Vourloumis is available for both photo assignments and freelance editing.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Greece mobile: +(30) 6989-8080-80</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">e-mail: <a href=\"mailto:[email protected]\" target=\"_blank\">[email protected]</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">current location: Athens, Greece</span></p>","textWidth":100},{"id":1786071268641,"type":"metadata","visible":true,"position":1,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":24}];
1714window.galleries = galleries;
1715
1716function getSiteGalleriesArray() {
1717  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
1718    return galleries;
1719  }
1720  if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
1721    return window.galleries;
1722  }
1723  if (window.GALLERY_DATA_CACHE &&
1724      window.GALLERY_DATA_CACHE.initialized &&
1725      Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
1726    return window.GALLERY_DATA_CACHE.galleries;
1727  }
1728  return [];
1729}
1730window.getSiteGalleriesArray = getSiteGalleriesArray;
1731
1732let activeGalleryId = null;
1733let sortableInstance = null;
1734let isEditing = false;
1735let initialPageInfo = {"id":1786070383351,"type":"gallery","pageId":"cwd7vhoc","slug":"book","url":"/book"};
1736
1737
1738// Store the current menu layout type (must be JSON-encoded — bare sidebar/horizontal is invalid JS)
1739let currentMenuLayout = null;
1740
1741/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */
1742function isNeonPageEmbedShell() {
1743  try {
1744    if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed'))
1745      return true;
1746    var _spEmbed = new URLSearchParams(window.location.search);
1747    return _spEmbed.get('neon_embed') === '1';
1748  } catch (_eEmbed) { return false; }
1749}
1750
1751/**
1752 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing),
1753 * otherwise use each gallery/page flag.
1754 */
1755function shouldApplyMenuHiddenForEmbedOrGallery(gallery) {
1756  var editing = typeof window.isInEditMode === 'function' && window.isInEditMode();
1757  if (editing)
1758    return !!(gallery && gallery.hideMenuOnPage);
1759  return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell();
1760}
1761
1762function shouldApplyMenuOverlayOnPage(gallery) {
1763  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery))
1764    return false;
1765  return !!(gallery && gallery.overlayMenuOnPage);
1766}
1767
1768function isOverlayMenuChromeActive() {
1769  return document.body.classList.contains('menu-overlay-on-page')
1770    && !document.body.classList.contains('edit-mode-active');
1771}
1772
1773/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */
1774function syncOverlayMenuInlineChrome() {
1775  var sidebar = document.querySelector('.sidebar');
1776  var header = document.querySelector('.mobile-header');
1777  if (isOverlayMenuChromeActive()) {
1778    if (sidebar) {
1779      sidebar.style.setProperty('background-color', 'transparent', 'important');
1780      sidebar.style.setProperty('background-image', 'none', 'important');
1781    }
1782    if (header) {
1783      header.style.setProperty('background-color', 'transparent', 'important');
1784      header.style.setProperty('background-image', 'none', 'important');
1785    }
1786    return;
1787  }
1788  if (sidebar) {
1789    var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa';
1790    var hidden = document.body.classList.contains('menu-background-hidden');
1791    sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important');
1792    if (hidden) {
1793      sidebar.style.setProperty('background-image', 'none', 'important');
1794    } else {
1795      sidebar.style.removeProperty('background-image');
1796    }
1797  }
1798  if (header) {
1799    header.style.removeProperty('background-color');
1800    header.style.removeProperty('background-image');
1801  }
1802}
1803
1804function applyMenuOverlayClassForGallery(gallery) {
1805  if (shouldApplyMenuOverlayOnPage(gallery)) {
1806    document.body.classList.add('menu-overlay-on-page');
1807  } else {
1808    document.body.classList.remove('menu-overlay-on-page');
1809  }
1810  syncOverlayMenuInlineChrome();
1811}
1812
1813function hookMenuStyleCustomizerForOverlay() {
1814  var customizer = window.MenuStyleCustomizer;
1815  if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function')
1816    return;
1817  var original = customizer.applyStyles.bind(customizer);
1818  customizer.applyStyles = function() {
1819    original.apply(this, arguments);
1820    if (isOverlayMenuChromeActive())
1821      syncOverlayMenuInlineChrome();
1822  };
1823  customizer.__overlayBgHooked = true;
1824}
1825window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery;
1826
1827document.addEventListener('DOMContentLoaded', () => {
1828  if (window.hydraInitialized) {
1829    console.log("Hydra already initialized, skipping");
1830    return;
1831  }
1832  window.hydraInitialized = true;
1833  document.body.classList.add('hydra-initialized');
1834  try {
1835    if (sessionStorage.getItem('hydra_refresh_cleared') === '1') {
1836      sessionStorage.removeItem('hydra_refresh_cleared');
1837      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
1838        window.EnhancedGallerySystem.resetInitializationState();
1839      }
1840      console.log('[Hydra] Applied ?refresh=1 — gallery framework reset for clean load.');
1841    }
1842  } catch (e) {}
1843  try {
1844    var neonEmbedParams = new URLSearchParams(window.location.search);
1845    if (neonEmbedParams.get('neon_embed') === '1') {
1846      document.body.classList.add('neon-page-embed', 'menu-hidden-on-page');
1847    }
1848  } catch (_neonEmbedErr) {}
1849  hookMenuStyleCustomizerForOverlay();
1850  setTimeout(hookMenuStyleCustomizerForOverlay, 0);
1851  setTimeout(hookMenuStyleCustomizerForOverlay, 500);
1852  if (isOverlayMenuChromeActive())
1853    syncOverlayMenuInlineChrome();
1854  window.Parameters = window.Parameters || {};
1855  if (localStorage.getItem('hydra_is_admin') === 'true') {
1856    document.body.classList.add('hydra-admin', 'hydra-authenticated');
1857    isAdmin = true;
1858    isAuthenticated = true;
1859    console.log('Restored admin status from localStorage');
1860    
1861    // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins
1862    // This ensures admins always see fresh data when opening new tabs
1863    try {
1864      console.log('Admin detected - clearing all caches except auth tokens...');
1865      
1866      // Save auth tokens first
1867      const savedAuthToken = localStorage.getItem('hydra_auth_token');
1868      const savedAuthEmail = localStorage.getItem('hydra_auth_email');
1869      const savedIsAdmin = localStorage.getItem('hydra_is_admin');
1870      const savedDidToken = localStorage.getItem('hydra_did_token');
1871      
1872      // Clear ALL localStorage
1873      localStorage.clear();
1874      
1875      // Restore auth tokens
1876      if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken);
1877      if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail);
1878      if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin);
1879      if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken);
1880      
1881      // Clear ALL sessionStorage (auth tokens are not stored here)
1882      sessionStorage.clear();
1883      
1884      console.log('Cleared all caches except auth tokens for admin user');
1885    } catch (cacheError) {
1886      console.warn('Error clearing caches on admin page load:', cacheError);
1887      // Don't fail page load if cache clearing fails
1888    }
1889  }
1890
1891  function ensureSidebarElementsAndRender() {
1892  console.log('Ensuring sidebar elements are available before rendering...');
1893  
1894  // Restore from siteConfig when hydrate missed or init invented a default menu
1895  if (window.SidebarManager) {
1896    var current = window.SidebarManager.elements;
1897    var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements;
1898    if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) {
1899      console.log('Initializing SidebarManager elements from siteConfig');
1900      window.SidebarManager.elements = siteConfig.sidebarElements;
1901      window.SidebarManager._elementsReady = true;
1902      window.SidebarManager._createdDefaultElements = false;
1903      if (typeof window.SidebarManager.renderElements === 'function') {
1904        window.SidebarManager.renderElements();
1905      }
1906    }
1907  }
1908  
1909  // Determine layout and render
1910  const currentMenuLayout = getEffectiveMenuLayout();
1911  
1912  if (currentMenuLayout === 'horizontal') {
1913    console.log('Rendering horizontal menu with ensured sidebar elements');
1914    renderHorizontalMenu();
1915  } else {
1916    renderGalleries();
1917  }
1918}
1919
1920 setTimeout(() => {
1921  console.log("Initializing galleries:", galleries.length);
1922  // Collapse folders before first paint so nested items never flash as expanded plain text
1923  alignFolderStates();
1924  
1925  // CHANGED: Use the new function instead of direct rendering
1926  setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init
1927  
1928  initMobileMenu();
1929  
1930  /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */
1931  let bootstrapRouteAttempted = false;
1932
1933  // Handle direct URL navigation
1934  if (initialPageInfo) {
1935    console.log("Loading initial page from direct URL:", initialPageInfo);
1936    
1937    // Find the gallery by ID
1938    const gallery = galleries.find(g => g.id === initialPageInfo.id);
1939    
1940    if (gallery) {
1941      // Set active gallery ID
1942      activeGalleryId = gallery.id;
1943      
1944      if (initialPageInfo.type === 'page') {
1945        console.log("Loading page directly:", gallery.title);
1946        loadPage(gallery.id);
1947      } else {
1948        console.log("Loading gallery directly:", gallery.title);
1949        loadGallery(gallery.id);
1950      }
1951      bootstrapRouteAttempted = true;
1952    }
1953  } else if (window.location.pathname === '/') {
1954    // No specific path, try to load home page
1955    console.log("No specific path, checking for home page");
1956    if (!loadHomePage() && galleries.length > 0) {
1957      // Default to first gallery if no home page is set
1958      loadGallery(galleries[0].id);
1959    }
1960    bootstrapRouteAttempted = true;
1961  } else if (galleries.length > 0) {
1962    // Path exists but no matching initialPageInfo, try to handle URL path
1963    handleURLNavigation();
1964    bootstrapRouteAttempted = true;
1965  }
1966  window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted;
1967  
1968  // Log what was rendered
1969  console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found");
1970  
1971    // Start with edit controls properly set
1972    toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active'));
1973    
1974    // Explicitly hide the import classic form too
1975    const importForm = document.getElementById('importClassicForm');
1976    if (importForm) {
1977      importForm.style.display = 'none';
1978      importForm.classList.remove('visible');
1979    }
1980    
1981    // Try to restore token from localStorage
1982    if (!didToken) {
1983      didToken = localStorage.getItem('hydra_auth_token');
1984      if (didToken) {
1985        console.log('Restored auth token from localStorage');
1986      }
1987    }
1988    
1989    // Initialize Magic and check authentication
1990    
1991    
1992    // Initialize sidebar elements if available
1993    if (window.SidebarManager) {
1994      // Pass the initial elements from server config if available
1995      if (true) {
1996        window.SidebarManager.elements = [{"id":1786066235717,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1786070996551,"type":"image","title":"Image","visible":true,"position":0,"imageUrl":"https://storage.neonsky.app/q7u17cy4/1786071007089_eirini.png","imageKey":"q7u17cy4/1786071007089_eirini.png","imageFilename":"eirini.png","imageType":"image/png","imageWidth":"68%","imageAlignment":"left","altText":"preview.neonsky.app","linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","linkAutoAdvance":false,"hideFromMobileMenu":true,"colorizeWithSiteColors":false}];
1997        if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
1998          window.SidebarManager._elementsReady = true;
1999          window.SidebarManager._createdDefaultElements = false;
2000        }
2001      }
2002      window.SidebarManager.init();
2003    }
2004
2005
2006    // Initialize metadata from server config (site-wide only — not current page SEO)
2007window.siteMetadata = {"title":"Eirini Vourloumis ","description":"Eirini Vourloumis is available for both photo assignments and freelance editing.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""};
2008console.log('Client Initialized site metadata (second init):', window.siteMetadata);
2009console.log('Client Google Analytics from metadataToUse (second init):', '');
2010console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics);
2011console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage);
2012
2013    // Update copyright footer after metadata is set (important for main domain)
2014    if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
2015      window.SidebarManager.updateMetadataFooter();
2016    }
2017
2018
2019
2020    const submenuCheck = document.getElementById('createSubmenu');
2021    const submenuTitleField = document.getElementById('submenuTitle');
2022    const submenuTitleGroup = submenuTitleField?.parentElement;
2023    
2024    if (submenuCheck && submenuTitleGroup) {
2025      submenuCheck.addEventListener('change', function() {
2026        submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
2027      });
2028    }
2029    
2030    // Check authentication state after initialization
2031    if (isAuthenticated && isAdmin) {
2032      console.log('Already authenticated and admin, showing edit UI');
2033      forceShowEditUI();
2034    } else if (didToken) {
2035      console.log('Have token, checking admin status');
2036      checkAuth();
2037    }
2038  }, 100); // Small delay to ensure everything is loaded
2039
2040  setTimeout(() => {
2041    if (window.location.pathname === '/' ||
2042        window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' ||
2043        (window.location.hostname === 'preview.neonsky.app' && 
2044         window.location.pathname.split('/').length <= 2)) {
2045      // Root URL or preview URL with only GUID - check for home page
2046      // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load)
2047      if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) {
2048        loadHomePage();
2049      }
2050    }
2051  }, 500);
2052
2053  // Dedicated editor URL: open edit tools or OTP login without requiring + key
2054  setTimeout(function hydraManageEditorEntry() {
2055    var p = window.location.pathname;
2056    if (p !== '/_manage' && p !== '/_manage/') return;
2057    try {
2058      if (sessionStorage.getItem('hydra_logout_cleared') === '1') {
2059        sessionStorage.removeItem('hydra_logout_cleared');
2060        console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.');
2061        return;
2062      }
2063    } catch (e) {}
2064    if (document.body.classList.contains('edit-mode-active')) return;
2065    try {
2066      var token = window.didToken || localStorage.getItem('hydra_auth_token');
2067      var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true';
2068      if (token && admin) {
2069        if (typeof window.toggleEditMode === 'function') window.toggleEditMode();
2070        return;
2071      }
2072      if (token && typeof checkAdminStatus === 'function') {
2073        checkAdminStatus(token).then(function() {
2074          if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) {
2075            window.toggleEditMode();
2076          }
2077        }).catch(function() {});
2078        return;
2079      }
2080      if (typeof startOTPLogin === 'function') {
2081        var pr = startOTPLogin();
2082        if (pr && typeof pr.catch === 'function') pr.catch(function() {});
2083      }
2084    } catch (e) {
2085      console.error('hydraManageEditorEntry:', e);
2086    }
2087  }, 2200);
2088});
2089
2090/**
2091 * Accessible name for header/sidebar brand images (no visual change).
2092 * Prefers element title or site metadata over generic "Image" / filenames.
2093 */
2094function resolveBrandLogoAlt(element) {
2095  if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') {
2096    return window.SidebarManager._resolveBrandImageAlt(element || {});
2097  }
2098  const siteTitle =
2099    (window.siteMetadata && String(window.siteMetadata.title || '').trim()) ||
2100    (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') ||
2101    String(location.hostname || '').replace(/^www\./i, '');
2102  const title = element && element.title ? String(element.title).trim() : '';
2103  if (title && !/^image(\s*\d*)?$/i.test(title)) return title;
2104  return siteTitle || 'Site logo';
2105}
2106
2107/**
2108 * Gets the first visible sidebar element (image or text) to be used as a logo or title.
2109 * @returns {object|null} The first suitable sidebar element or null.
2110 */
2111
2112function getFirstSidebarElementForHeader() {
2113    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2114        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
2115        if (visibleElements.length > 0) {
2116            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
2117            
2118            // Debug: Log all element types found
2119            const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title }));
2120            console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes);
2121            
2122            for (let el of sortedElements) {
2123                if (el.type === 'image' || el.type === 'text') { // Filter 2: type
2124                    console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId });
2125                    return el;
2126                }
2127            }
2128            // If loop finishes, no visible image or text found among visible elements
2129            console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type));
2130            return null;
2131        } else {
2132            console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length);
2133            return null;
2134        }
2135    }
2136    console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
2137    return null;
2138}
2139
2140/**
2141 * Gets the first visible social icon element from sidebar elements.
2142 * @returns {object|null} The first suitable social element or null.
2143 */
2144function getFirstSocialElementForHeader() {
2145    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2146        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
2147        if (visibleElements.length > 0) {
2148            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
2149            for (let el of sortedElements) {
2150                if (el.type === 'social') { // Filter for social type
2151                    return el;
2152                }
2153            }
2154            // If loop finishes, no visible social element found
2155            console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.');
2156            return null;
2157        } else {
2158            console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.');
2159            return null;
2160        }
2161    }
2162    console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
2163    return null;
2164}
2165
2166function isSpaceColonySiteTheme() {
2167  const themeId = document.body && document.body.dataset.siteTheme;
2168  return themeId === 'space-colony';
2169}
2170
2171/**
2172 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu.
2173 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked.
2174 */
2175function applyHorizontalLogoMenuLayoutStyles() {
2176  const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings;
2177  const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline';
2178  const isSpaceColonyTheme = isSpaceColonySiteTheme();
2179  document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked');
2180  const w = document.querySelector('.logo-menu-container');
2181  if (w) {
2182    const isMobile = window.innerWidth <= 768;
2183    w.style.display = 'flex';
2184    w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center';
2185    if (mode === 'stacked') {
2186      w.style.flexDirection = 'column';
2187      w.style.gap = isMobile ? '8px' : '12px';
2188      w.style.width = '100%';
2189      w.style.boxSizing = 'border-box';
2190      w.style.flexGrow = isMobile ? '0' : '1';
2191      w.style.minWidth = '0';
2192      w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start';
2193    } else {
2194      w.style.flexDirection = 'row';
2195      w.style.gap = '20px';
2196      w.style.width = '';
2197      w.style.boxSizing = '';
2198      w.style.minWidth = '';
2199      if (isMobile) {
2200        w.style.flexGrow = '0';
2201        w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
2202      } else {
2203        w.style.flexGrow = '';
2204        w.style.justifyContent = '';
2205      }
2206    }
2207  }
2208  const mhc = document.querySelector('.mobile-header-content');
2209  if (mhc) {
2210    const isMobile = window.innerWidth <= 768;
2211    if (isSpaceColonyTheme && !isMobile) {
2212      mhc.style.alignItems = 'flex-end';
2213      mhc.style.flexGrow = '0';
2214      mhc.style.width = '';
2215    } else if (isSpaceColonyTheme && isMobile) {
2216      mhc.style.alignItems = 'flex-start';
2217      mhc.style.justifyContent = 'flex-start';
2218      mhc.style.flexGrow = '';
2219      mhc.style.width = '100%';
2220    } else if (!isMobile && mode === 'stacked') {
2221      mhc.style.alignItems = 'flex-start';
2222      mhc.style.flexGrow = '';
2223      mhc.style.width = '';
2224    } else {
2225      mhc.style.alignItems = '';
2226      mhc.style.flexGrow = '';
2227      mhc.style.width = '';
2228    }
2229  }
2230  if (typeof window.syncContentMarginToHorizontalHeader === 'function') {
2231    window.syncContentMarginToHorizontalHeader();
2232  }
2233}
2234window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles;
2235
2236function parseHorizontalHeaderHeightPx(value, fallback) {
2237  if (value === null || value === undefined) {
2238    return fallback;
2239  }
2240  const parsed = parseInt(String(value).trim(), 10);
2241  if (Number.isNaN(parsed) || parsed < 1) {
2242    return fallback;
2243  }
2244  return parsed;
2245}
2246
2247function getUserHorizontalHeaderHeightPx() {
2248  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2249    const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0);
2250    if (fromSetting > 0) {
2251      return fromSetting;
2252    }
2253  }
2254  const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim();
2255  const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0);
2256  if (parsedVar > 0) {
2257    return parsedVar;
2258  }
2259  const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim();
2260  const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0);
2261  if (parsedTheme > 0) {
2262    return parsedTheme;
2263  }
2264  return 60;
2265}
2266
2267function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) {
2268  const headerVal = headerPx + 'px';
2269  const marginVal = contentMarginPx + 'px';
2270  const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim();
2271  const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim();
2272  if (currentHeader === headerVal && currentMargin === marginVal) {
2273    return;
2274  }
2275  document.documentElement.style.setProperty('--mobile-header-height', headerVal);
2276  document.documentElement.style.setProperty('--content-margin-top', marginVal);
2277}
2278
2279function measureHorizontalHeaderNaturalHeight(header) {
2280  const el = header;
2281  const prevHeight = el.style.getPropertyValue('height');
2282  const prevMinHeight = el.style.getPropertyValue('min-height');
2283  /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap
2284     doesn’t floor the measurement when the viewport widens back to one row. */
2285  el.style.setProperty('height', 'auto', 'important');
2286  el.style.setProperty('min-height', '0', 'important');
2287  const measured = Math.ceil(el.getBoundingClientRect().height);
2288  if (prevHeight) {
2289    el.style.setProperty('height', prevHeight);
2290  } else {
2291    el.style.removeProperty('height');
2292  }
2293  if (prevMinHeight) {
2294    el.style.setProperty('min-height', prevMinHeight);
2295  } else {
2296    el.style.removeProperty('min-height');
2297  }
2298  return measured;
2299}
2300
2301function applyHorizontalHeaderContentInset() {
2302  if (!document.body.classList.contains('menu-layout-horizontal')) {
2303    return;
2304  }
2305  if (window._horizontalHeaderInsetApplying) {
2306    return;
2307  }
2308  const header = document.querySelector('.mobile-header');
2309  if (!header) {
2310    return;
2311  }
2312
2313  window._horizontalHeaderInsetApplying = true;
2314  try {
2315    const userH = getUserHorizontalHeaderHeightPx();
2316    /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh;
2317       leaving that on --top-bar-height-setting makes calc(var(...) - 8px) → 0 and hides the logo. */
2318    document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px');
2319    const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open');
2320    const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked');
2321    const isPhone = window.innerWidth <= 768;
2322
2323    /*
2324     * Phone hamburger chrome: fixed Style Customizer height (link row is hidden).
2325     * Desktop inline may wrap beside the logo — measure like stacked so the bar can grow.
2326     * Stacked / open submenu: always measure.
2327     */
2328    if (!hasExpandedMenu && !isStackedLogo && isPhone) {
2329      setHorizontalHeaderHeightVars(userH, userH);
2330      return;
2331    }
2332
2333    /* Desktop inline (wrap-capable), stacked logo, or open submenu */
2334    const measured = measureHorizontalHeaderNaturalHeight(header);
2335    if (!measured || measured < 1) {
2336      setHorizontalHeaderHeightVars(userH, userH);
2337      return;
2338    }
2339
2340    let maxH = Math.max(200, Math.round(window.innerHeight * 0.5));
2341    const clamped = Math.min(Math.max(measured, userH), maxH);
2342    /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */
2343    const contentMargin = clamped > userH ? clamped + 8 : clamped;
2344    setHorizontalHeaderHeightVars(clamped, contentMargin);
2345  } finally {
2346    window._horizontalHeaderInsetApplying = false;
2347  }
2348}
2349
2350function ensureHorizontalHeaderHeightObserver() {
2351  if (typeof ResizeObserver === 'undefined') {
2352    return;
2353  }
2354  const header = document.querySelector('.mobile-header');
2355  if (!header) {
2356    return;
2357  }
2358  if (window._horizontalHeaderResizeObserver) {
2359    window._horizontalHeaderResizeObserver.disconnect();
2360  }
2361  window._horizontalHeaderResizeObserver = new ResizeObserver(function() {
2362    applyHorizontalHeaderContentInset();
2363  });
2364  window._horizontalHeaderResizeObserver.observe(header);
2365}
2366
2367function syncContentMarginToHorizontalHeader() {
2368  if (!document.body.classList.contains('menu-layout-horizontal')) {
2369    return;
2370  }
2371  requestAnimationFrame(function() {
2372    requestAnimationFrame(function() {
2373      applyHorizontalHeaderContentInset();
2374      ensureHorizontalHeaderHeightObserver();
2375    });
2376  });
2377}
2378window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader;
2379window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset;
2380
2381function getEffectiveMenuLayout() {
2382  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') {
2383    return window.MenuStyleCustomizer.getEffectiveMenuLayout();
2384  }
2385  if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal';
2386  if (document.body.classList.contains('menu-layout-top')) return 'top';
2387  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2388    return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2389  }
2390  return 'sidebar';
2391}
2392
2393function ensureCorrectLayoutApplied() {
2394  // Check if MenuStyleCustomizer and its necessary parts exist
2395  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2396    // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics
2397    const fromSettings = window.MenuStyleCustomizer.settings.menuLayout;
2398    const currentLayout = fromSettings || getEffectiveMenuLayout();
2399    console.log(`Ensuring layout is applied: ${currentLayout}`);
2400
2401    // Apply the layout styles (CSS classes, display properties)
2402    window.MenuStyleCustomizer._applyMenuLayout(currentLayout);
2403
2404    // --- Force Header Structure Rebuild for Horizontal Layout ---
2405    if (currentLayout === 'horizontal') {
2406      const mobileHeader = document.querySelector('.mobile-header');
2407      if (mobileHeader) {
2408        let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2409        // Ensure the main content container exists
2410        if (!mobileHeaderContent) {
2411          mobileHeaderContent = document.createElement('div');
2412          mobileHeaderContent.className = 'mobile-header-content';
2413          const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn');
2414          if (hamburgerBtn) {
2415            mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn);
2416          } else {
2417            mobileHeader.appendChild(mobileHeaderContent);
2418          }
2419        }
2420        // Ensure specific sub-containers exist, creating them if necessary
2421        // This guarantees the structure is present before renderHorizontalMenu fills them.
2422        if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) {
2423            const logoContainer = document.createElement('div');
2424            logoContainer.className = 'horizontal-header-logo';
2425            mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild);
2426        }
2427        if (!mobileHeaderContent.querySelector('.horizontal-menu
2427-container')) {
2428            const menuContainer = document.createElement('div');
2429            menuContainer.className = 'horizontal-menu-container';
2430            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2431            if (logoContainer) {
2432                logoContainer.insertAdjacentElement('afterend', menuContainer);
2433            } else { // Fallback if logo container wasn't found/created
2434                 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild);
2435            }
2436        }
2437        // Apply flexbox styling to position social container at far right
2438        mobileHeaderContent.style.display = 'flex';
2439        const isSpaceColonyTheme = isSpaceColonySiteTheme();
2440        const isMobile = window.innerWidth <= 768;
2441        mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2442        mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2443        if (isSpaceColonyTheme && !isMobile) {
2444          mobileHeaderContent.style.flexGrow = '0';
2445          mobileHeaderContent.style.width = '';
2446        } else {
2447          mobileHeaderContent.style.flexGrow = '';
2448          mobileHeaderContent.style.width = '100%';
2449        }
2450        
2451        // Create a flex container for logo and menu items
2452        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2453        if (!logoMenuContainer) {
2454            console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...');
2455            const logoMenuWrapper = document.createElement('div');
2456            logoMenuWrapper.className = 'logo-menu-container';
2457            // Move logo and menu into the wrapper
2458            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2459            const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
2460            
2461            console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer);
2462            console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer);
2463            
2464            if (logoContainer && menuContainer) {
2465                // Insert wrapper before the logo
2466                mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2467                // Move both containers into the wrapper
2468                logoMenuWrapper.appendChild(logoContainer);
2469                logoMenuWrapper.appendChild(menuContainer);
2470                console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper');
2471            } else {
2472                console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper');
2473            }
2474        } else {
2475            console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists');
2476        }
2477        if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2478          applyHorizontalLogoMenuLayoutStyles();
2479        }
2480        
2481        // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created)
2482        if (!mobileHeaderContent.querySelector('.horizontal-social-container')) {
2483            const socialContainer = document.createElement('div');
2484            socialContainer.className = 'horizontal-social-container';
2485            // Insert at the end (far right) of the mobile header content
2486            mobileHeaderContent.appendChild(socialContainer);
2487        }
2488      }
2489    }
2490     // --- End Header Structure Rebuild ---
2491
2492
2493    // Re-render the correct menu structure based on the applied layout
2494    if (currentLayout === 'horizontal') {
2495      // If horizontal layout, render the horizontal menu
2496      if (typeof renderHorizontalMenu === 'function') {
2497        renderHorizontalMenu();
2498      } else {
2499        console.warn("renderHorizontalMenu function not found.");
2500      }
2501      
2502      // After rendering horizontal menu, update mobile header content
2503      // This ensures mobile header shows logo instead of menu items on mobile
2504      setTimeout(() => {
2505        if (typeof updateMobileHeaderContent === 'function') {
2506          updateMobileHeaderContent();
2507        }
2508      }, 100);
2509    } else {
2510      // For sidebar or top layouts, render the standard gallery tree
2511      if (typeof renderGalleries === 'function') {
2512        renderGalleries();
2513      } else {
2514        console.warn("renderGalleries function not found.");
2515      }
2516      
2517      // Update mobile header content for other layouts too
2518      setTimeout(() => {
2519        if (typeof updateMobileHeaderContent === 'function') {
2520          updateMobileHeaderContent();
2521        }
2522      }, 100);
2523    }
2524  } else {
2525    // Log a warning if the necessary components aren't available
2526    console.warn("MenuStyleCustomizer or its methods not available to re-apply layout.");
2527  }
2528}
2529
2530function applyLayoutStylesOnly() {
2531    // Check if MenuStyleCustomizer and its necessary parts exist
2532    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2533        // Get the current layout setting, default to 'sidebar' if not set
2534        const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2535        console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`);
2536        // Apply the layout styles (CSS classes, display properties)
2537        window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles
2538    } else {
2539        console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available.");
2540    }
2541}
2542
2543// Add this function to ensure sidebar content for mobile
2544function ensureSidebarContentForMobile() {
2545    const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
2546    const isMobileView = window.innerWidth <= 768;
2547    
2548    if (isHorizontalLayout && isMobileView) {
2549        console.log('Ensuring sidebar content for mobile horizontal layout');
2550        
2551        // Make sure the sidebar tree is populated
2552        const galleryTreeContainer = document.getElementById('galleryTree');
2553        if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) {
2554            console.log('Sidebar tree is empty, populating with galleries');
2555            
2556            // Render the galleries in the sidebar tree for mobile navigation
2557            if (typeof renderGalleries === 'function') {
2558                renderGalleries();
2559            }
2560        }
2561        
2562        // Also ensure sidebar elements are rendered
2563        if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') {
2564            console.log('Re-rendering sidebar elements for mobile');
2565            window.SidebarManager.renderElements();
2566        }
2567    }
2568}
2569
2570function renderTextLogo(container, htmlContentFromSidebarElement) {
2571  if (!container) {
2572    console.error("[RenderTextLogo] Error: Logo container not found.");
2573    return;
2574  }
2575
2576  container.innerHTML = '';
2577
2578  const raw = (htmlContentFromSidebarElement || '').trim();
2579  if (!raw) {
2580    container.innerHTML = '<span class="site-title-text">Menu</span>';
2581  } else if (!/<[a-z][sS]*>/i.test(raw)) {
2582    // Plain text — wrap so theme CSS can target .site-title-text
2583    container.innerHTML = '<span class="site-title-text">' + raw + '</span>';
2584  } else {
2585    container.innerHTML = raw;
2586    const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
2587    if (titleEl && !titleEl.classList.contains('site-title-text')) {
2588      titleEl.classList.add('site-title-text');
2589    }
2590  }
2591
2592  // Do not force heading/paragraph margins to 0 — menu-styles.css matches
2593  // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter.
2594
2595  console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`);
2596}
2597
2598function renderHorizontalMenu() {
2599  console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined');
2600  
2601  // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts)
2602  const currentLayout = getEffectiveMenuLayout();
2603  
2604  const isHorizontalLayout = currentLayout === 'horizontal';
2605  console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout);
2606  
2607  // If not horizontal layout, only handle logo/alignment, then return early
2608  if (!isHorizontalLayout) {
2609    console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation");
2610    // Still handle logo/alignment for sidebar layout
2611    const mobileHeader = document.querySelector('.mobile-header');
2612    if (mobileHeader) {
2613      const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2614      if (logoContainer) {
2615        // Handle logo rendering only (code below will handle this)
2616        // But we'll return before creating menu items
2617      }
2618    }
2619    // Return early - don't create horizontal menu items in sidebar layout
2620    return;
2621  }
2622  
2623  // Wait for sidebar elements to be available before rendering
2624  if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) {
2625    console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting...");
2626    const waitForSidebarElements = (attempts = 0) => {
2627      const maxAttempts = 20; // Increased attempts for slower loads
2628      if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2629        console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render.");
2630        renderHorizontalMenu();
2631      } else if (attempts < maxAttempts) {
2632        const attemptNum = attempts + 1;
2633        setTimeout(() => waitForSidebarElements(attempts + 1), 100);
2634      } else {
2635        console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway.");
2636        // Continue with render even if sidebar elements aren't loaded
2637      }
2638    };
2639    waitForSidebarElements();
2640    return;
2641  }
2642  
2643  const mobileHeader = document.querySelector('.mobile-header');
2644  if (!mobileHeader) {
2645    console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND.");
2646    return;
2647  }
2648
2649  // Apply flexbox styling to position social container at far right
2650  const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2651  if (mobileHeaderContent) {
2652    mobileHeaderContent.style.display = 'flex';
2653    const isSpaceColonyTheme = isSpaceColonySiteTheme();
2654    const isMobile = window.innerWidth <= 768;
2655    mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2656    mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2657    if (isSpaceColonyTheme && !isMobile) {
2658      mobileHeaderContent.style.flexGrow = '0';
2659      mobileHeaderContent.style.width = '';
2660    } else {
2661      mobileHeaderContent.style.flexGrow = '';
2662      mobileHeaderContent.style.width = '100%';
2663    }
2664    
2665    // Create a flex container for logo and menu items
2666    const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2667    if (!logoMenuContainer) {
2668      console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...');
2669      const logoMenuWrapper = document.createElement('div');
2670      logoMenuWrapper.className = 'logo-menu-container';
2671      // Move logo and menu into the wrapper
2672      const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2673      const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2673-container');
2674      
2675      console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer);
2676      console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer);
2677      
2678      if (logoContainer && menuContainer) {
2679        // Insert wrapper before the logo
2680        mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2681        // Move both containers into the wrapper
2682        logoMenuWrapper.appendChild(logoContainer);
2683        logoMenuWrapper.appendChild(menuContainer);
2684        console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper');
2685      } else {
2686        console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper');
2687      }
2688    } else {
2689      console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists');
2690    }
2691    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2692      applyHorizontalLogoMenuLayoutStyles();
2693    }
2694  }
2695
2696  const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2697  if (!logoContainer) {
2698    console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header.");
2699    return;
2700  }
2701  
2702  console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container");
2703  logoContainer.innerHTML = ''; // Clear existing logo content first
2704
2705  // FIXED: Check if we're on mobile - if so, don't render the horizontal logo
2706  const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed
2707  
2708  if (isMobileView) {
2709    console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering");
2710    // On mobile, the logo will be handled by updateMobileHeaderContent()
2711  } else {
2712    console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo");
2713    
2714    // Get the logo element data
2715    let logoElementData = null;
2716    try {
2717      logoElementData = getFirstSidebarElementForHeader();
2718      console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData));
2719    } catch (error) {
2720      console.error('[RenderHorizontalMenu] Error getting sidebar element:', error);
2721      logoElementData = getFirstSidebarElementFallback();
2722    }
2723
2724     if (logoElementData) {
2725      if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) {
2726        console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl);
2727        
2728        const img = document.createElement('img');
2729        img.src = logoElementData.imageUrl || logoElementData.imageData;
2730        img.alt = resolveBrandLogoAlt(logoElementData);
2731        img.className = 'sidebar-image';
2732        
2733        // Apply comprehensive styling to ensure visibility
2734        img.style.display = 'block';
2735        
2736        // --- FIX ---
2737        // The line below was removed. It was applying a fixed max-height of 50px,
2738        // overriding your CSS. Removing it allows your stylesheet to take control.
2739        // img.style.maxHeight = '50px'; 
2740        
2741        img.style.objectFit = 'contain';
2742        img.style.minWidth = '1px';
2743        img.style.minHeight = '0';
2744
2745        let logoDisplayNode = img;
2746        if (logoElementData.colorizeWithSiteColors &&
2747            window.SidebarManager &&
2748            typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
2749          const buildData = Object.assign({}, logoElementData);
2750          if (!buildData.imageWidth) {
2751            buildData.imageWidth = 'auto';
2752          }
2753          logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
2754        } else {
2755          img.style.width = 'auto';
2756          img.style.visibility = 'visible';
2757          img.style.opacity = '1';
2758        }
2759
2760        // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS)
2761        if (window.SidebarManager &&
2762            typeof window.SidebarManager._wrapWithImagePadding === 'function') {
2763          logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData);
2764        }
2765
2766        // Enhanced event listeners for debugging
2767        img.onload = () => {
2768          console.log('[RenderHorizontalMenu] Image successfully LOADED');
2769        };
2770        
2771        img.onerror = (error) => {
2772          console.error('[RenderHorizontalMenu] Image FAILED to load:', error);
2773          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2774        };
2775
2776        try {
2777          // Handle linking (similar to sidebar-manager.js)
2778          // Only add links in view mode, not edit mode
2779          // Explicit edit toggle only — Parameters.isInEditor is also true for logged-in admins
2780          const isEditing = document.body.classList.contains('edit-mode-active') ||
2781                           (typeof window.isInEditMode === 'function' && window.isInEditMode());
2782          
2783          // Debug: Log link properties and element data
2784          console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData);
2785          console.log('[RenderHorizontalMenu] Image link properties:', {
2786            linkType: logoElementData.linkType,
2787            linkUrl: logoElementData.linkUrl,
2788            linkPageId: logoElementData.linkPageId,
2789            linkTarget: logoElementData.linkTarget,
2790            isEditing: isEditing,
2791            hasLinkType: !!logoElementData.linkType,
2792            linkTypeValue: logoElementData.linkType,
2793            linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none',
2794            notEditing: !isEditing,
2795            bodyHasEditClass: document.body.classList.contains('edit-mode-active'),
2796            isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A',
2797            ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A'
2798          });
2799          
2800          // Check for link - handle undefined, null, empty string, or 'none'
2801          const hasLink = logoElementData.linkType && 
2802                         logoElementData.linkType !== 'none' && 
2803                         logoElementData.linkType !== '' &&
2804                         !isEditing;
2805          
2806          console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType);
2807          
2808          if (hasLink) {
2809            const link = document.createElement('a');
2810            link.target = logoElementData.linkTarget || '_self';
2811            let finalLinkHref = '#';
2812            let isValidLink = false;
2813            
2814            if (logoElementData.linkType === 'external' && logoElementData.linkUrl) {
2815              finalLinkHref = logoElementData.linkUrl;
2816              if (link.target === '_blank') {
2817                link.rel = 'noopener noreferrer';
2818              }
2819              isValidLink = true;
2820            } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2821              const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2822              const linkPageIdNum = parseInt(logoElementData.linkPageId);
2823              console.log('[RenderHorizontalMenu] Looking for internal page:', {
2824                linkPageId: logoElementData.linkPageId,
2825                linkPageIdNum: linkPageIdNum,
2826                allGalleriesLength: allGalleries ? allGalleries.length : 0,
2827                galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : []
2828              });
2829              
2830              const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2831              
2832              console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null);
2833              
2834              if (targetPage) {
2835                // Handle preview mode URLs
2836                const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
2837                console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode);
2838                
2839                if (isPreviewMode) {
2840                  const pathParts = window.location.pathname.split('/').filter(Boolean);
2841                  const siteGuid = pathParts[0];
2842                  console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug);
2843                  
2844                  if (siteGuid && targetPage.slug) {
2845                    finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
2846                    isValidLink = true;
2847                    console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref);
2848                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2849                    finalLinkHref = targetPage.url;
2850                    isValidLink = true;
2851                    console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref);
2852                  }
2853                } else {
2854                  if (targetPage.slug) {
2855                    finalLinkHref = '/' + targetPage.slug;
2856                    isValidLink = true;
2857                    console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref);
2858                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2859                    finalLinkHref = targetPage.url;
2860                    isValidLink = true;
2861                    console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref);
2862                  }
2863                }
2864              } else {
2865                console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId);
2866              }
2867            }
2868            
2869            if (isValidLink) {
2870              link.href = finalLinkHref;
2871              // Ensure link is clickable
2872              link.style.pointerEvents = 'auto';
2873              link.style.cursor = 'pointer';
2874              link.style.textDecoration = 'none';
2875              link.style.display = 'flex';
2876              link.style.alignItems = 'center';
2877              link.style.height = 'auto';
2878              link.style.maxHeight = '100%';
2879              link.style.minHeight = '0';
2880              link.style.lineHeight = '0';
2881              console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied');
2882              
2883              // For internal links, add click handler to use site navigation
2884              if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2885                // Store linkPageId in a variable that won't be affected by closure
2886                const linkPageId = logoElementData.linkPageId;
2887                const linkHref = finalLinkHref;
2888                
2889                // Add click handler with both capture and bubble phases to ensure it fires
2890                const clickHandler = (e) => {
2891                  console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e);
2892                  e.preventDefault();
2893                  e.stopPropagation();
2894                  e.stopImmediatePropagation();
2895                  
2896                  const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2897                  const linkPageIdNum = parseInt(linkPageId);
2898                  const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2899                  
2900                  console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null);
2901                  console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery);
2902                  
2903                  if (targetPage) {
2904                    if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
2905                      console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id);
2906                      window.loadPage(targetPage.id);
2907                    } else if (typeof window.loadGallery === 'function') {
2908                      console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id);
2909                      window.loadGallery(targetPage.id);
2910                    } else {
2911                      // Fallback to standard navigation
2912                      console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref);
2913                      window.location.href = linkHref;
2914                    }
2915                  } else {
2916                    // Fallback to standard navigation if page not found
2917                    console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref);
2918                    window.location.href = linkHref;
2919                  }
2920                  
2921                  return false;
2922                };
2923                
2924                // Add handler in capture phase to catch event early
2925                link.addEventListener('click', clickHandler, true);
2926                // Also add in bubble phase as backup
2927                link.addEventListener('click', clickHandler, false);
2928                
2929                // Also add mousedown as backup in case click is being prevented
2930                link.addEventListener('mousedown', (e) => {
2931                  if (e.button === 0) { // Left click only
2932                    console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler');
2933                    clickHandler(e);
2934                  }
2935                });
2936                
2937                console.log('[RenderHorizontalMenu] Click handlers attached to link');
2938              }
2939              
2940              link.appendChild(logoDisplayNode);
2941              logoContainer.appendChild(link);
2942              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer);
2943              
2944              // Verify link is in DOM and clickable
2945              setTimeout(() => {
2946                const verifyLink = logoContainer.querySelector('a');
2947                if (verifyLink) {
2948                  console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', {
2949                    href: verifyLink.href,
2950                    pointerEvents: window.getComputedStyle(verifyLink).pointerEvents,
2951                    cursor: window.getComputedStyle(verifyLink).cursor,
2952                    zIndex: window.getComputedStyle(verifyLink).zIndex,
2953                    position: window.getComputedStyle(verifyLink).position
2954                  });
2955                } else {
2956                  console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!');
2957                }
2958              }, 100);
2959            } else {
2960              console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink);
2961              logoContainer.appendChild(logoDisplayNode);
2962              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
2963            }
2964          } else {
2965            logoContainer.appendChild(logoDisplayNode);
2966            console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
2967          }
2968        } catch (e) {
2969          console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e);
2970          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2971        }
2972        
2973      } else if (logoElementData.type === 'text' && logoElementData.textContent) {
2974        console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent);
2975        renderTextLogo(logoContainer, logoElementData.textContent);
2976        
2977      } else {
2978        console.log('[RenderHorizontalMenu] Using title as fallback logo');
2979        renderTextLogo(logoContainer, logoElementData.title || 'Menu');
2980      }
2981    } else {
2982      console.log("[RenderHorizontalMenu] No logoElementData available. Using default text.");
2983      renderTextLogo(logoContainer, 'Menu');
2984    }
2985  }
2986
2987  // Render menu items (only on desktop, not on mobile)
2988  const menuContainer = mobileHeader.querySelector('.horizontal-menu-container');
2989  if (!menuContainer) {
2990    console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header.");
2991  } else {
2992    menuContainer.innerHTML = '';
2993    
2994    // Apply alignment class from settings
2995    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2996      const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left';
2997      const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left';
2998      menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center');
2999      menuContainer.classList.add('menu-align-' + alignment);
3000      console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-' 
3000+ alignment);
3001    }
3002    
3003    // Only populate menu items on desktop, not on mobile
3004    if (!isMobileView) {
3005      let galleryTree = [];
3006      if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') {
3007        galleryTree = createGalleryTree(galleries);
3008      } else {
3009        console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined.");
3010      }
3011      
3012      galleryTree.forEach(item => {
3013        const menuItemElement = createHorizontalMenuItem(item);
3014        if (menuItemElement) {
3015          menuContainer.appendChild(menuItemElement);
3016        }
3017      });
3018      if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
3019    } else {
3020      console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population");
3021    }
3022  }
3023
3024  // Render social icons (only on desktop, not on mobile)
3025  let socialContainer = mobileHeader.querySelector('.horizontal-social-container');
3026  if (!socialContainer) {
3027    // Fallback: create the social container if it doesn't exist
3028    console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it...");
3029    const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
3030    if (mobileHeaderContent) {
3031      socialContainer = document.createElement('div');
3032      socialContainer.className = 'horizontal-social-container';
3033      mobileHeaderContent.appendChild(socialContainer);
3034      console.log("[RenderHorizontalMenu] Created .horizontal-social-container");
3035    } else {
3036      console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container");
3037    }
3038  }
3039  
3040  if (socialContainer) {
3041    socialContainer.innerHTML = '';
3042    
3043    // Only populate social icons on desktop, not on mobile
3044    if (!isMobileView) {
3045      const socialElement = getFirstSocialElementForHeader();
3046      if (socialElement && socialElement.type === 'social') {
3047        console.log('[RenderHorizontalMenu] Found social element, rendering social icons');
3048        renderSocialIcons(socialContainer, socialElement);
3049      } else {
3050        console.log('[RenderHorizontalMenu] No social element found or element is not social type');
3051        // Container remains empty - no impact on layout
3052      }
3053    } else {
3054      console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population");
3055    }
3056  }
3057
3058
3059
3060  if (typeof updateActiveStatesHorizontal === 'function') {
3061    updateActiveStatesHorizontal();
3062  }
3063
3064  if (!isMobileView && mobileHeaderContent) {
3065    mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) {
3066      if (!el.closest('.horizontal-header-logo')) {
3067        el.remove();
3068      }
3069    });
3070  }
3071  
3072setTimeout(() => {
3073  ensureSidebarContentForMobile();
3074}, 100);
3075
3076  if (typeof syncContentMarginToHorizontalHeader === 'function') {
3077    setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200);
3078  }
3079
3080  console.log("[RenderHorizontalMenu] Function COMPLETED.");
3081}
3082
3083/**
3084 * Renders social icons in the horizontal header container.
3085 * @param {HTMLElement} container - The container to render social icons in
3086 * @param {object} socialElement - The social element data from sidebar
3087 */
3088function renderSocialIcons(container, socialElement) {
3089  console.log('[RenderSocialIcons] Rendering social icons:', socialElement);
3090  
3091  try {
3092    // Safety check: ensure we have a valid social element
3093    if (!socialElement || socialElement.type !== 'social') {
3094      console.log('[RenderSocialIcons] Invalid social element - skipping rendering');
3095      return;
3096    }
3097
3098    /* Placeholders without URLs still need menu color (CSS also sets this). */
3099    container.style.color = 'var(--menu-color, #333)';
3100    
3101    // Get icon size with proper fallbacks
3102    let iconSize = 30;
3103    if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) {
3104      iconSize = socialElement.socialIconSize;
3105    } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) {
3106      iconSize = parseInt(socialElement.socialIconSize);
3107    }
3108    
3109    // Render icons directly from element data
3110    if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) {
3111      socialElement.socialIcons.forEach(icon => {
3112        // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible)
3113        const iconWrapper = document.createElement('div');
3114        iconWrapper.className = 'social-icon-wrapper';
3115        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;';
3116
3117        const applySvgSizing = (host) => {
3118          const svg = host.querySelector('svg');
3119          if (!svg) return;
3120          svg.style.width = '100%';
3121          svg.style.height = '100%';
3122          svg.setAttribute('width', '100%');
3123          svg.setAttribute('height', '100%');
3124          svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
3125          svg.setAttribute('aria-hidden', 'true');
3126          svg.setAttribute('focusable', 'false');
3127          svg.style.color = 'inherit';
3128          svg.style.fill = 'currentColor';
3129          svg.style.stroke = 'currentColor';
3130        };
3131
3132        const hasUrl = !!(icon.url && String(icon.url).trim());
3133
3134        // Add link if URL exists; otherwise still render the glyph (placeholder icons)
3135        if (hasUrl) {
3136          const link = document.createElement('a');
3137          link.href = icon.url;
3138          link.target = '_blank';
3139          link.rel = 'noopener noreferrer';
3140          link.classList.add('horizontal-social-link');
3141          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;';
3142          const socialLabel = (icon.alt && String(icon.alt).trim())
3143            || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function'
3144              ? window.SidebarManager._socialIconTypeLabel(icon.type)
3145              : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : ''))
3146            || 'Social link';
3147          const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1);
3148          link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)');
3149          
3150          if (icon.type === 'custom' && icon.iconUrl) {
3151            // Render uploaded image
3152            const img = document.createElement('img');
3153            img.src = icon.iconUrl;
3154            img.alt = '';
3155            img.setAttribute('aria-hidden', 'true');
3156            img.style.width = '100%';
3157            img.style.height = '100%';
3158            img.style.objectFit = 'contain';
3159            link.appendChild(img);
3160          } else {
3161            // Add SVG - use SidebarManager's method if available, otherwise create basic SVG
3162            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3163              link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3164            } else {
3165              // Fallback: create a simple SVG placeholder
3166              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>';
3167            }
3168            applySvgSizing(link);
3169          }
3170          iconWrapper.appendChild(link);
3171        } else {
3172          // Placeholder: no URL — still paint with menu color via container inheritance
3173          iconWrapper.setAttribute('aria-hidden', 'true');
3174          if (icon.type === 'custom' && icon.iconUrl) {
3175            const img = document.createElement('img');
3176            img.src = icon.iconUrl;
3177            img.alt = '';
3178            img.style.width = '100%';
3179            img.style.height = '100%';
3180            img.style.objectFit = 'contain';
3181            iconWrapper.appendChild(img);
3182          } else {
3183            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3184              iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3185            } else {
3186              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>';
3187            }
3188            applySvgSizing(iconWrapper);
3189          }
3190        }
3191        
3192        container.appendChild(iconWrapper);
3193        console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url);
3194      });
3195    } else {
3196      console.log('[RenderSocialIcons] No social icons found in element');
3197    }
3198  } catch (error) {
3199    console.error('[RenderSocialIcons] Error rendering social icons:', error);
3200  }
3201}
3202
3203/**
3204 * Gets the appropriate icon text or class for a social platform.
3205 * @param {string} platform - The social platform name
3206 * @returns {string} The icon text or class
3207 */
3208function getSocialIconText(platform) {
3209  const iconMap = {
3210    'facebook': 'f',
3211    'twitter': 't',
3212    'instagram': 'i',
3213    'linkedin': 'in',
3214    'youtube': 'yt',
3215    'pinterest': 'p',
3216    'tiktok': 'tt',
3217    'snapchat': 'sc',
3218    'whatsapp': 'wa',
3219    'telegram': 'tg',
3220    'discord': 'd',
3221    'github': 'gh',
3222    'email': '@',
3223    'phone': '📞',
3224    'website': '🌐'
3225  };
3226  
3227  return iconMap[platform.toLowerCase()] || '•';
3228}
3229
3230
3231
3232
3233/**
3234 * Creates a single top-level horizontal menu item and its submenu if it has children.
3235 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal)
3236 * @param {object} galleryItem - The gallery item data.
3237 * @returns {HTMLElement | null} The created menu item element, or null if not rendered.
3238 */
3239function createHorizontalMenuItem(galleryItem) {
3240  if (galleryItem.visible === false || galleryItem.isSpacer) {
3241      return null;
3242  }
3243
3244  const menuItem = document.createElement('div');
3245  menuItem.className = 'horizontal-menu-item';
3246  menuItem.dataset.id = String(galleryItem.id);
3247  menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3248
3249  const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length);
3250  const isNavigableParent = isNavigableMenuParent(galleryItem);
3251  let titleEl;
3252  if (hasVisibleChildren && !isNavigableParent) {
3253    titleEl = document.createElement('span');
3254    titleEl.className = 'horizontal-menu-label';
3255    titleEl.textContent = galleryItem.title;
3256    menuItem.setAttribute('aria-haspopup', 'true');
3257    menuItem.setAttribute('aria-expanded', 'false');
3258  } else {
3259    titleEl = document.createElement('a');
3260    titleEl.className = 'horizontal-menu-label';
3261    titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3262    titleEl.textContent = galleryItem.title;
3263    if (galleryItem.isExternal) {
3264      titleEl.target = '_blank';
3265      titleEl.rel = 'noopener noreferrer';
3266    } else {
3267      titleEl.addEventListener('click', (e) => { e.preventDefault(); });
3268    }
3269  }
3270  menuItem.appendChild(titleEl);
3271
3272  // Ensure activeGalleryId is treated as a number for comparison during initial render
3273  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3274  if (galleryItem.id === currentGlobalActiveId) {
3275      menuItem.classList.add('active');
3276  }
3277
3278  let leaveTimer; // For mouseleave timeout to close dropdown
3279
3280  if (galleryItem.children && galleryItem.children.length > 0) {
3281      const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer);
3282
3283      if (visibleChildren.length > 0) {
3284          menuItem.classList.add('has-children');
3285          if (isNavigableParent) {
3286              menuItem.setAttribute('aria-haspopup', 'true');
3287              menuItem.setAttribute('aria-expanded', 'false');
3288          }
3289
3290          const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span');
3291          toggleIconContainer.className = 'submenu-toggle-horizontal';
3292          if (isNavigableParent) {
3293              toggleIconContainer.type = 'button';
3294              toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3295              toggleIconContainer.setAttribute('aria-expanded', 'false');
3296              toggleIconContainer.addEventListener('click', function(event) {
3297                  event.preventDefault();
3298                  event.stopPropagation();
3299                  const willExpand = !menuItem.classList.contains('expanded');
3300                  menuItem.classList.toggle('expanded', willExpand);
3301                  menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3302                  toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3303                  const icon = toggleIconContainer.querySelector('.icon.toggle-icon');
3304                  if (icon) icon.classList.toggle('rotated', willExpand);
3305              });
3306          }
3307          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3308          svgIcon.setAttribute("class", "icon toggle-icon");
3309          svgIcon.setAttribute("viewBox", "0 0 24 24");
3310          svgIcon.setAttribute("fill", "none");
3311          svgIcon.setAttribute("stroke", "currentColor");
3312          svgIcon.setAttribute("stroke-width", "2");
3313          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3314          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3315          svgIcon.appendChild(polyline);
3316          toggleIconContainer.appendChild(svgIcon);
3317          menuItem.appendChild(toggleIconContainer);
3318
3319          const subMenu = document.createElement('div');
3320          subMenu.className = 'horizontal-dropdown';
3321          const subMenuList = document.createElement('ul');
3322          subMenuList.className = 'horizontal-dropdown-list';
3323
3324          visibleChildren.forEach((childItem) => {
3325              const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call
3326              if (subMenuItemElement) {
3327                  subMenuList.appendChild(subMenuItemElement);
3328              }
3329          });
3330
3331          if (subMenuList.hasChildNodes()) {
3332              subMenu.appendChild(subMenuList);
3333              menuItem.appendChild(subMenu);
3334
3335              menuItem.addEventListener('mouseenter', function() {
3336                  clearTimeout(leaveTimer);
3337                  document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3338                      if (openItem !== this) {
3339                          openItem.classList.remove('expanded');
3340                          openItem.setAttribute('aria-expanded', 'false');
3341                          const otherIcon = openItem.querySelector('.icon.toggle-icon');
3342                          if (otherIcon) otherIcon.classList.remove('rotated');
3343                      }
3344                  });
3345                  this.classList.add('expanded');
3346                  this.setAttribute('aria-expanded', 'true');
3347                  if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true');
3348                  const currentIcon = this.querySelector('.icon.toggle-icon');
3349                  if (currentIcon) currentIcon.classList.add('rotated');
3350              });
3351
3352              menuItem.addEventListener('mouseleave', function(event) {
3353                  // Check if we're moving to the dropdown or a flyout
3354                  const relatedTarget = event.relatedTarget;
3355                  const isMovingToDropdown = relatedTarget && (
3356                      relatedTarget.closest('.horizontal-dropdown') ||
3357                      relatedTarget.classList.contains('horizontal-submenu-item') ||
3358                      relatedTarget.closest('.horizontal-submenu-item')
3359                  );
3360                  const isMovingToFlyout = relatedTarget && (
3361                      relatedTarget.classList.contains('horizontal-nested-dropdown') ||
3362                      relatedTarget.closest('.horizontal-nested-dropdown')
3363                  );
3364                  
3365                  if (!isMovingToDropdown && !isMovingToFlyout) {
3366                      console.log('Main menu item mouseleave - starting timer');
3367                      leaveTimer = setTimeout(() => {
3368                          // Check if there's an active flyout - if so, keep parent open
3369                          // Use the same multiple detection methods as the submenu handler
3370                          const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3371                                             document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3372                                             document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3373                          console.log('Main menu timer fired - active flyout:', activeFlyout);
3374                          if (!activeFlyout) {
3375                              console.log('Main menu closing dropdown - no active flyout');
3376                              this.classList.remove('expanded');
3377                              this.setAttribute('aria-expanded', 'false');
3378                              if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false');
3379                              const currentIcon = this.querySelector('.icon.toggle-icon');
3380                              if (currentIcon) currentIcon.classList.remove('rotated');
3381                          } else {
3382                              console.log('Main menu keeping dropdown open - flyout is active');
3383                          }
3384                      }, 200);
3385                  } else {
3386                      console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)');
3387                  }
3388              });
3389
3390              subMenu.addEventListener('mouseenter', function() {
3391                  const parentMenuItem = this.closest('.horizontal-menu
3391-item');
3392                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3393                      clearTimeout(parentMenuItem.leaveTimer);
3394                      parentMenuItem.leaveTimer = null;
3395                  }
3396              });
3397
3398              subMenu.addEventListener('mouseleave', function(event) {
3399                  const parentMenuItem = this.closest('.horizontal-menu-item');
3400                  if (parentMenuItem) {
3401                      // Check if we're moving to another element within the same dropdown
3402                      const relatedTarget = event.relatedTarget;
3403                      console.log('SubMenu mouseleave - relatedTarget:', relatedTarget);
3404                      console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null');
3405                      
3406                      // Check if we're moving to another element within the same dropdown
3407                      const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown');
3408                      console.log('Current dropdown found:', !!currentDropdown);
3409                      console.log('Current dropdown element:', currentDropdown);
3410                      console.log('RelatedTarget:', relatedTarget);
3411                      console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null');
3412                      
3413                      // Check if relatedTarget is within the same dropdown container
3414                      const isMovingWithinDropdown = relatedTarget && currentDropdown && (
3415                          relatedTarget.closest('.horizontal-dropdown') === currentDropdown ||
3416                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3417                          relatedTarget.closest('.horizontal-submenu-item') ||
3418                          currentDropdown.contains(relatedTarget)
3419                      );
3420                      
3421                      // Alternative approach: check if relatedTarget is within the same parent menu item
3422                      const parentMenuContainer = this.closest('.horizontal-menu-item');
3423                      const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget);
3424                      console.log('Is moving within parent menu item:', isMovingWithinParent);
3425                      console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown');
3426                      console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null');
3427                      
3428                      // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside)
3429                      // Get the parent menu item's dropdown container
3430                      const parentMenuItem = this.closest('.horizontal-menu-item');
3431                      console.log('Parent menu item found:', !!parentMenuItem);
3432                      const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null;
3433                      console.log('Dropdown container found:', !!dropdownContainer);
3434                      if (dropdownContainer) {
3435                          console.log('Dropdown container classes:', dropdownContainer.className);
3436                          console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display);
3437                          console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position);
3438                      }
3439                      
3440                      // Get bounds from the visible dropdown (not the hidden one)
3441                      let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 };
3442                      if (dropdownContainer) {
3443                          const computedStyle = window.getComputedStyle(dropdownContainer);
3444                          if (computedStyle.display !== 'none') {
3445                              dropdownRect = dropdownContainer.getBoundingClientRect();
3446                          } else {
3447                              // If dropdown is hidden, try to get bounds from the parent menu item
3448                              const parentRect = parentMenuItem.getBoundingClientRect();
3449                              dropdownRect = parentRect;
3450                          }
3451                      }
3452                      const mouseX = event.clientX;
3453                      const mouseY = event.clientY;
3454                      const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 
3455                                                     mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom;
3456                      
3457                      console.log('Dropdown bounds:', {
3458                          left: dropdownRect.left,
3459                          right: dropdownRect.right,
3460                          top: dropdownRect.top,
3461                          bottom: dropdownRect.bottom,
3462                          mouseX,
3463                          mouseY,
3464                          mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom
3465                      });
3466                      
3467                      // If there's an active flyout, always keep the parent dropdown open
3468                      // Check multiple ways to detect active flyouts
3469                      const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3470                                         document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3471                                         document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3472                      console.log('Active flyout found:', !!activeFlyout);
3473                      if (activeFlyout) {
3474                          console.log('Active flyout display style:', activeFlyout.style.display);
3475                          console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display);
3476                          console.log('Active flyout visibility style:', activeFlyout.style.visibility);
3477                      }
3478                      const hasActiveFlyout = !!activeFlyout;
3479                      
3480                      // If the dropdown is hidden (display: none), we can't get accurate bounds
3481                      // So let's use a simpler approach - if we're moving to another submenu item, keep open
3482                      const isMovingToSubmenuItem = relatedTarget && (
3483                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3484                          relatedTarget.closest('.horizontal-submenu-item')
3485                      );
3486                      console.log('Is moving to submenu item:', isMovingToSubmenuItem);
3487                      console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null');
3488                      console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null');
3489                      
3490                      // If we're moving to another submenu item within the same dropdown, al
3490ways keep open
3491                      // OR if there's an active flyout (regardless of where the mouse is), keep open
3492                      // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open
3493                      const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem;
3494                      
3495                      console.log('Is moving within dropdown:', isMovingWithinDropdown);
3496                      console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds);
3497                      console.log('Has active flyout:', hasActiveFlyout);
3498                      console.log('Should keep open:', shouldKeepOpen);
3499                      
3500                      if (!shouldKeepOpen) {
3501                          console.log('SubMenu mouseleave - starting timer (moving outside dropdown)');
3502                          parentMenuItem.leaveTimer = setTimeout(() => {
3503                              // Check if there's an active flyout - if so, keep parent open
3504                              const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3505                              console.log('Timer fired - active flyout:', activeFlyout);
3506                              if (!activeFlyout) {
3507                                  console.log('Closing parent dropdown - no active flyout');
3508                                  parentMenuItem.classList.remove('expanded');
3509                                  const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3510                                  if (currentIcon) currentIcon.classList.remove('rotated');
3511                              } else {
3512                                  console.log('Keeping parent dropdown open - flyout is active');
3513                              }
3514                          }, 200);
3515                      } else {
3516                          console.log('SubMenu mouseleave - not starting timer (moving within dropdown)');
3517                      }
3518                  }
3519              });
3520          } else {
3521              menuItem.classList.remove('has-children');
3522              if (toggleIconContainer.parentNode) {
3523                  toggleIconContainer.remove();
3524              }
3525          }
3526      }
3527  }
3528
3529  // Click listener for items that are direct navigation links (not folders with children)
3530  if (!menuItem.classList.contains('has-children') || isNavigableParent) {
3531      menuItem.addEventListener('click', async (event) => {
3532          if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) {
3533              return;
3534          }
3535          event.stopPropagation();
3536          const clickedItemId = galleryItem.id; // Capture the ID of the clicked item
3537
3538          // Check if it's an external URL first
3539          if (galleryItem.isExternal && galleryItem.url) {
3540              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3541              return;
3542          }
3543
3544          if (isPageMenuItem(galleryItem)) {
3545              if (typeof loadPage === 'function') {
3546                  loadPage(clickedItemId, event); // loadPage is synchronous
3547                  if (typeof updateActiveStatesHorizontal === 'function') {
3548                      console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3549                      // Use a minimal delay even for sync operations if page rendering has its own async microtasks
3550                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3551                  }
3552              }
3553          } else { // It's a gallery
3554              if (typeof loadGallery === 'function') {
3555                  console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3556                  await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery
3557                  console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3558                  if (typeof updateActiveStatesHorizontal === 'function') {
3559                      setTimeout(() => {
3560                          console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3561                          updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId
3562                      }, 100); // Delay to allow gallery system to settle
3563                  }
3564              }
3565          }
3566
3567          // Close any other open dropdowns and flyouts
3568          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3569              // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click)
3570              if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) {
3571                 openItem.classList.remove('expanded');
3572                 openItem.setAttribute('aria-expanded', 'false');
3573                 const icon = openItem.querySelector('.icon.toggle-icon');
3574                 if (icon) icon.classList.remove('rotated');
3575              }
3576          });
3577          
3578          // Close all flyouts
3579          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3580              flyout.style.display = 'none';
3581              flyout.style.opacity = '0';
3582              flyout.style.visibility = 'hidden';
3583          });
3584          
3585          // Remove expanded class from all submenu items
3586          document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => {
3587              subItem.classList.remove('expanded');
3588          });
3589      });
3590  } else {
3591      // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior
3592      menuItem.setAttribute('tabindex', '0');
3593      menuItem.addEventListener('keydown', function(event) {
3594          if (event.key !== 'Enter' && event.key !== ' ') return;
3595          event.preventDefault();
3596          const isOpen = this.classList.contains('expanded');
3597          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3598              if (openItem !== this) {
3599                  openItem.classList.remove('expanded');
3600                  openItem.setAttribute('aria-expanded', 'false');
3601                  const otherIcon = openItem.querySelector('.icon.toggle-icon');
3602                  if (otherIcon) otherIcon.classList.remove('rotated');
3603              }
3604          });
3605          this.classList.toggle('expanded', !isOpen);
3606          this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false');
3607          const currentIcon = this.querySelector('.icon.toggle-icon');
3608          if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen);
3609      });
3610  }
3611  return menuItem;
3612}
3613
3614/**
3615* Creates a single horizontal submenu item.
3616* (UPDATED to handle nested children and async loadGallery)
3617* @param {object} galleryItem - The gallery item data for the submenu.
3618* @returns {HTMLElement | null} The created submenu item element, or null if not rendered.
3619*/
3620function createHorizontalSubMenuItem(galleryItem) {
3621  if (galleryItem.visible === false || galleryItem.isSpacer) {
3622      return null;
3623  }
3624
3625  const subMenuItem = document.createElement('li');
3626  subMenuItem.className = 'horizontal-submenu-item';
3627  subMenuItem.dataset.id = String(galleryItem.id);
3628  subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3629
3630  const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer);
3631  const isNavigableParent = isNavigableMenuParent(galleryItem);
3632  let titleSpan;
3633  if (subVisibleChildren.length > 0 && !isNavigableParent) {
3634      titleSpan = document.createElement('span');
3635      titleSpan.className = 'horizontal-menu-label';
3636      titleSpan.textContent = galleryItem.title;
3637  } else {
3638      titleSpan = document.createElement('a');
3639      titleSpan.className = 'horizontal-menu-label';
3640      titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3641      titleSpan.textContent = galleryItem.title;
3642      if (galleryItem.isExternal) {
3643          titleSpan.target = '_blank';
3644          titleSpan.rel = 'noopener noreferrer';
3645      } else {
3646          titleSpan.addEventListener('click', (e) => { e.preventDefault(); });
3647      }
3648  }
3649  subMenuItem.appendChild(titleSpan);
3650
3651  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3652  if (galleryItem.id === currentGlobalActiveId) {
3653      subMenuItem.classList.add('active');
3654      if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page');
3655  }
3656
3657  // Check if this submenu item has children (nested sub-sub items)
3658  if (galleryItem.children && galleryItem.children.length > 0) {
3659      const visibleChildren = subVisibleChildren;
3660      
3661      if (visibleChildren.length > 0) {
3662          subMenuItem.classList.add('has-children');
3663          
3664          // Add arrow indicator for nested items (same as top-level menu)
3665          const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span');
3666          arrowSpan.className = 'submenu-arrow';
3667          if (isNavigableParent) {
3668              arrowSpan.type = 'button';
3669              arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3670              arrowSpan.setAttribute('aria-expanded', 'false');
3671              arrowSpan.addEventListener('click', function(event) {
3672                  event.preventDefault();
3673                  event.stopPropagation();
3674                  const isOpen = subMenuItem.classList.contains('expanded');
3675                  if (isOpen) {
3676                      closeHorizontalNestedFlyout(subMenuItem);
3677                  } else {
3678                      openHorizontalNestedFlyout(subMenuItem);
3679                  }
3680              });
3681          }
3682          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3683          svgIcon.setAttribute("class", "icon toggle-icon");
3684          svgIcon.setAttribute("viewBox", "0 0 24 24");
3685          svgIcon.setAttribute("fill", "none");
3686          svgIcon.setAttribute("stroke", "currentColor");
3687          svgIcon.setAttribute("stroke-width", "2");
3688          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3689          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3690          svgIcon.appendChild(polyline);
3691          arrowSpan.appendChild(svgIcon);
3692          subMenuItem.appendChild(arrowSpan);
3693          
3694          // Create nested submenu as a true flyout
3695          const nestedSubMenu = document.createElement('div');
3696          nestedSubMenu.className = 'horizontal-nested-dropdown';
3697          
3698          // Create a list container for the nested items
3699          const nestedList = document.createElement('ul');
3700          nestedList.className = 'horizontal-nested-list';
3701          
3702          visibleChildren.forEach((childItem) => {
3703              const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call
3704              if (nestedSubMenuItem) {
3705                  nestedList.appendChild(nestedSubMenuItem);
3706              }
3707          });
3708          
3709          nestedSubMenu.appendChild(nestedList);
3710          
3711          if (nestedSubMenu.hasChildNodes()) {
3712              // Append to document body for true flyout behavior
3713              document.body.appendChild(nestedSubMenu);
3714              // Store reference to the flyout for cleanup
3715              const flyoutId = 'flyout-' + Date.now() + '-' + Math.random();
3716              subMenuItem.setAttribute('data-flyout-id', flyoutId);
3717              nestedSubMenu.setAttribute('data-flyout-id', flyoutId);
3718              
3719              console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug
3720              console.log('Flyout element:', nestedSubMenu); // Debug
3721              
3722              // Add hover events for nested submenu
3723              let nestedLeaveTimer;
3724
3725              // Shared open/close used by hover AND keyboard — mouse path unchanged
3726              function openHorizontalNestedFlyout(hostItem) {
3727                  if (!hostItem) return;
3728                  if (hostItem.nestedLeaveTimer) {
3729                      clearTimeout(hostItem.nestedLeaveTimer);
3730                  }
3731                  const parentMenuItem = hostItem.closest('.horizontal-menu-item');
3732                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3733                      clearTimeout(parentMenuItem.leaveTimer);
3734                  }
3735                  document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => {
3736                      if (openItem !== hostItem) {
3737                          openItem.classList.remove('expanded');
3738                          openItem.setAttribute('aria-expanded', 'false');
3739                          const otherFlyoutId = openItem.getAttribute('data-flyout-id');
3740                          if (otherFlyoutId) {
3741                              const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]');
3742                              if (otherFlyout) {
3743                                  otherFlyout.style.display = 'none';
3744                                  otherFlyout.style.opacity = '0';
3745                                  otherFlyout.style.visibility = 'hidden';
3746                              }
3747                          }
3748                      }
3749                  });
3750                  hostItem.classList.add('expanded');
3751                  hostItem.setAttribute('aria-expanded', 'true');
3752                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3753                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true');
3754                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3755                  if (!hostFlyoutId) return;
3756                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3757                  if (!nestedDropdown) return;
3758                  const rect = hostItem.getBoundingClientRect();
3759                  const parentDropdown = hostItem.closest('.horizontal-dropdown');
3760                  const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect;
3761                  const leftPos = parentRect.right - 1;
3762                  const topPos = rect.top;
3763                  nestedDropdown.style.left = leftPos + 'px';
3764                  nestedDropdown.style.top = topPos + 'px';
3765                  nestedDropdown.style.display = 'block';
3766                  nestedDropdown.style.opacity = '1';
3767                  nestedDropdown.style.visibility = 'visible';
3768                  nestedDropdown.style.position = 'fixed';
3769                  nestedDropdown.style.zIndex = '1251';
3770                  setTimeout(() => {
3771                      const dropdownRect = nestedDropdown.getBoundingClientRect();
3772                      if (dropdownRect.right > window.innerWidth) {
3773                          nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px';
3774                      }
3775                  }, 10);
3776              }
3777
3778              function closeHorizontalNestedFlyout(hostItem) {
3779                  if (!hostItem) return;
3780                  hostItem.classList.remove('expanded');
3781                  hostItem.setAttribute('aria-expanded', 'false');
3782                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3783                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false');
3784                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3785                  if (!hostFlyoutId) return;
3786                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3787                  if (nestedDropdown) {
3788                      nestedDropdown.style.display = 'none';
3789                      nestedDropdown.style.opacity = '0';
3790                      nestedDropdown.style.visibility = 'hidden';
3791                  }
3792              }
3793
3794              // Keyboard opens the same flyout hover shows — attrs on label so mouse click focus stays natural
3795              if (titleSpan && !isNavigableParent) {
3796                  titleSpan.setAttribute('tabindex', '0');
3797                  titleSpan.setAttribute('role', 'button');
3798                  titleSpan.setAttribute('aria-haspopup', 'true');
3799                  titleSpan.setAttribute('aria-expanded', 'false');
3800                  titleSpan.addEventListener('keydown', function(event) {
3801                      if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') {
3802                          event.preventDefault();
3803                          event.stopPropagation();
3804                          openHorizontalNestedFlyout(subMenuItem);
3805                          titleSpan.setAttribute('aria-expanded', 'true');
3806                          const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id');
3807                          const nestedDropdown = hostFlyoutId
3808                              ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]')
3809                              : null;
3810                          const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3810-label');
3811                          if (firstLink && typeof firstLink.focus === 'function') {
3812                              try { firstLink.focus(); } catch (e) {}
3813                          }
3814                      } else if (event.key === 'Escape' || event.key === 'ArrowLeft') {
3815                          event.preventDefault();
3816                          event.stopPropagation();
3817                          closeHorizontalNestedFlyout(subMenuItem);
3818                          titleSpan.setAttribute('aria-expanded', 'false');
3819                      }
3820                  });
3821              }
3822              
3823              subMenuItem.addEventListener('mouseenter', function() {
3824                  openHorizontalNestedFlyout(this);
3825                  if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true');
3826              });
3827              
3828              subMenuItem.addEventListener('mouseleave', function(event) {
3829                  // Check if we're moving to the flyout
3830                  const flyoutId = this.getAttribute('data-flyout-id');
3831                  const relatedTarget = event.relatedTarget;
3832                  const isMovingToFlyout = relatedTarget && flyoutId && 
3833                      relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]');
3834                  
3835                  // Also check if we're moving within the dropdown
3836                  const parentMenuItem = this.closest('.horizontal-menu-item');
3837                  const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget);
3838                  
3839                  console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout);
3840                  console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown);
3841                  
3842                  if (!isMovingToFlyout && !isMovingWithinDropdown) {
3843                      this.nestedLeaveTimer = setTimeout(() => {
3844                          closeHorizontalNestedFlyout(this);
3845                          if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false');
3846                          
3847                          // Now that the flyout is closed, allow the parent dropdown to close
3848                          const parentMenuItem = this.closest('.horizontal-menu-item');
3849                          if (parentMenuItem) {
3850                              setTimeout(() => {
3851                                  const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3852                                  if (!activeFlyout) {
3853                                      parentMenuItem.classList.remove('expanded');
3854                                      const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3855                                      if (currentIcon) currentIcon.classList.remove('rotated');
3856                                  }
3857                              }, 50);
3858                          }
3859                      }, 200);
3860                  } else {
3861                      console.log('Submenu item mouseleave - not starting timer (moving to flyout)');
3862                  }
3863              });
3864              
3865              nestedSubMenu.addEventListener('mouseenter', function() {
3866                  console.log('Flyout mouseenter - clearing timers');
3867                  clearTimeout(nestedLeaveTimer);
3868                  
3869                  // Clear the parent dropdown's leave timer to keep it open
3870                  const flyoutId = this.getAttribute('data-flyout-id');
3871                  if (flyoutId) {
3872                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3873                      if (parentSubItem) {
3874                          const parentMenuItem = parentSubItem.closest('.horizontal-menu-item');
3875                          if (parentMenuItem && parentMenuItem.leaveTimer) {
3876                              console.log('Clearing parent menu item leave timer');
3877                              clearTimeout(parentMenuItem.leaveTimer);
3878                              parentMenuItem.leaveTimer = null;
3879                          } else {
3880                              console.log('No parent menu item leave timer to clear');
3881                          }
3882                          if (parentSubItem.nestedLeaveTimer) {
3883                              console.log('Clearing submenu item nested leave timer');
3884                              clearTimeout(parentSubItem.nestedLeaveTimer);
3885                              parentSubItem.nestedLeaveTimer = null;
3886                          }
3887                      }
3888                  }
3889              });
3890              
3891              nestedSubMenu.addEventListener('mouseleave', function() {
3892                  const flyoutId = this.getAttribute('data-flyout-id');
3893                  if (flyoutId) {
3894                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3895                      if (parentSubItem) {
3896                          nestedLeaveTimer = setTimeout(() => {
3897                              closeHorizontalNestedFlyout(parentSubItem);
3898                              const label = parentSubItem.querySelector('.horizontal-menu
3898-label');
3899                              if (label) label.setAttribute('aria-expanded', 'false');
3900                          }, 200);
3901                      }
3902                  }
3903              });
3904          }
3905      }
3906  }
3907
3908  // Click listener for navigation (only if no children or if it's a direct link)
3909  if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) {
3910      subMenuItem.addEventListener('click', async (event) => {
3911          if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) {
3912              return;
3913          }
3914          event.stopPropagation();
3915          const clickedItemId = galleryItem.id;
3916
3917          // Check if it's an external URL first
3918          if (galleryItem.isExternal && galleryItem.url) {
3919              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3920              return;
3921          }
3922
3923          if (isPageMenuItem(galleryItem)) {
3924              if (typeof loadPage === 'function') {
3925                  loadPage(clickedItemId, event);
3926                  if (typeof updateActiveStatesHorizontal === 'function') {
3927                      console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3928                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3929                  }
3930              }
3931          } else { // It's a gallery
3932              if (typeof loadGallery === 'function') {
3933                  console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3934                  await loadGallery(clickedItemId, event);
3935                  console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3936                  if (typeof updateActiveStatesHorizontal === 'function') {
3937                      setTimeout(() => {
3938                          console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3939                          updateActiveStatesHorizontal(clickedItemId);
3940                      }, 100);
3941                  }
3942              }
3943          }
3944
3945          // Close all parent dropdowns and flyouts after navigation
3946          document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => {
3947              openItem.classList.remove('expanded');
3948              const icon = openItem.querySelector('.icon.toggle-icon');
3949              if (icon) icon.classList.remove('rotated');
3950          });
3951          
3952          // Close all flyouts
3953          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3954              flyout.style.display = 'none';
3955              flyout.style.opacity = '0';
3956              flyout.style.visibility = 'hidden';
3957          });
3958      });
3959  }
3960  
3961  return subMenuItem;
3962}
3963
3964/**
3965* Updates the 'active' class for items in the horizontal menu.
3966*/
3967
3968function updateActiveStatesHorizontal(forcedActiveId = null) {
3969  // Prioritize forcedActiveId if provided, otherwise use global state.
3970  // Ensure the ID is treated as a number for comparison.
3971  const targetId = forcedActiveId !== null 
3972                   ? parseInt(String(forcedActiveId)) 
3973                   : parseInt(String(window.activeGalleryId || activeGalleryId));
3974
3975  console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 
3976              (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 
3977              "Type:", typeof targetId);
3978
3979  const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item');
3980
3981  if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number
3982    console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId);
3983    return;
3984  }
3985
3986  let activeItemSet = false;
3987  menuItems.forEach(item => {
3988    const itemIdStr = item.dataset.id;
3989    if (!itemIdStr) {
3990      // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item);
3991      return; 
3992    }
3993    const itemId = parseInt(itemIdStr);
3994
3995    if (itemId === targetId) {
3996      if (!item.classList.contains('active')) {
3997        item.classList.add('active');
3998        console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
3999      }
4000      activeItemSet = true;
4001    } else {
4002      if (item.classList.contains('active')) {
4003        item.classList.remove('active');
4004        console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
4005      }
4006    }
4007  });
4008
4009  if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number
4010      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.`);
4011  }
4012
4013  const activeSubItem = document.querySelector('.horizontal-submenu-item.active');
4014  if (activeSubItem) {
4015      const parentTopItem = activeSubItem.closest('.horizontal-menu
4015-item.has-children');
4016      if (parentTopItem && !parentTopItem.classList.contains('active')) {
4017          // parentTopItem.classList.add('active-parent'); // Optional class for styling parent
4018      }
4019  }
4020  console.log("[Horizontal Menu Update] Completed for Target ID:", targetId);
4021}
4022
4023
4024  // Listener for closing dropdowns when clicking outside
4025  document.addEventListener('click', function(event) {
4026      if (currentMenuLayout === 'horizontal') {
4027          const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded');
4028          let clickedInsideSubmenuOrParent = false;
4029
4030          openSubmenus.forEach(submenuContainer => {
4031              // Check if click is on the submenu container OR its direct parent menu item
4032              if (submenuContainer.contains(event.target) || 
4033                  (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) {
4034                  clickedInsideSubmenuOrParent = true;
4035              }
4036          });
4037          
4038          // If click is on another top-level item that is NOT expanded, also don't close
4039          const targetMenuItem = event.target.closest('.horizontal-menu-item');
4040          if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) {
4041               clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it
4042          }
4043
4044
4045          if (!clickedInsideSubmenuOrParent) {
4046              openSubmenus.forEach(submenuContainer => {
4047                  submenuContainer.classList.remove('expanded');
4048              });
4049          }
4050      }
4051  });
4052
4053document.addEventListener('menuLayoutChanged', function(e) {
4054    const newLayout = e.detail.layout;
4055    console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing);
4056
4057    // Update the global currentMenuLayout variable if you have one
4058    if (typeof currentMenuLayout !== 'undefined') {
4059        currentMenuLayout = newLayout;
4060    }
4061
4062    const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected
4063
4064    // Always clear and re-render the appropriate menu structure for the new layout.
4065    // This is important for style changes (like font size) that affect dimensions.
4066    const galleryTreeContainer = document.getElementById('galleryTree');
4067    const mobileHeaderContent = document.querySelector('.mobile-header-content');
4068
4069    if (galleryTreeContainer) {
4070        galleryTreeContainer.innerHTML = ''; // Clear existing tree
4071    }
4072    if (mobileHeaderContent) {
4073        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
4074        if (horizontalMenuContainer) {
4075            horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself
4076        }
4077    }
4078
4079    // Render the correct menu structure based on the new layout
4080    if (newLayout === 'horizontal') {
4081        if (typeof renderHorizontalMenu === 'function') {
4082            renderHorizontalMenu();
4083        }
4084        // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal
4085    } else { // 'sidebar' or 'top'
4086        if (typeof renderGalleries === 'function') {
4087            renderGalleries();
4088        }
4089        // CSS rules will handle sidebar visibility
4090    }
4091
4092    // If currently in edit mode, ensure the UI components for editing are correctly displayed
4093    // for the new layout, without fully toggling the mode off and on.
4094    if (isEditing && typeof window.updateGlobalEditState === 'function') {
4095        console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.');
4096
4097        // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode.
4098        // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar.
4099        window.updateGlobalEditState(true);
4100
4101        // 2. Re-initialize sortables. It's often safer to destroy existing ones first.
4102        if (typeof initializeNestedSortables === 'function') {
4103            if (typeof destroyNestedSortables === 'function') {
4104                destroyNestedSortables();
4105            }
4106            setTimeout(initializeNestedSortables, 100); // Delay for DOM updates
4107        }
4108        // Also reinitialize for SidebarManager if it exists and handles its own sortables
4109        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
4110             setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150);
4111        }
4112
4113        // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
4114        const editControls = document.querySelector('.edit-controls');
4115        if (editControls) {
4116            editControls.style.display = 'flex'; // Or your default display type for these controls
4117            editControls.classList.add('visible');
4118        }
4119
4120        // 4. If the layout is now horizontal, the sidebar (for editing) should be visible.
4121        // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }`
4122        // should handle this. This log is for confirmation.
4123        if (newLayout === 'horizontal' && sidebar) {
4124            console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.');
4125        }
4126        
4127        // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set.
4128        if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 
4129            window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) {
4130            window.PageManager.setEditMode(true);
4131        }
4132
4133    } else if (!isEditing) {
4134        // If not in edit mode, CSS should handle the view state.
4135        // This block can be used for any explicit view mode adjustments if CSS isn't sufficient.
4136        console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.');
4137    }
4138
4139    // Update mobile title and close mobile menu if open, regardless of edit mode
4140    if (typeof updateMobileTitle === 'function') {
4141        updateMobileTitle();
4142    }
4143    if (typeof closeMobileMenu === 'function') {
4144        closeMobileMenu();
4145    }
4146
4147    // Re-render the active page so row-band insets and recipe layout match the new menu shell
4148    if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) {
4149        console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId);
4150        window.PageManager.renderElements(window.PageManager.currentPageId);
4151    }
4152    if (typeof ensureCorrectLayoutApplied === 'function') {
4153        setTimeout(ensureCorrectLayoutApplied, 0);
4154    }
4155});
4156
4157
4158// Make sure getFirstSidebarElementForHeader is available if called from other scripts
4159if (typeof window.getFirstSidebarElementForHeader === 'undefined') {
4160  window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader;
4161}
4162
4163// Make sure getFirstSocialElementForHeader is available if called from other scripts
4164if (typeof window.getFirstSocialElementForHeader === 'undefined') {
4165  window.getFirstSocialElementForHeader = getFirstSocialElementForHeader;
4166}
4167
4168// Make sure renderSocialIcons is available if called from other scripts
4169if (typeof window.renderSocialIcons === 'undefined') {
4170  window.renderSocialIcons = renderSocialIcons;
4171}
4172if (typeof window.renderHorizontalMenu === 'undefined') {
4173    window.renderHorizontalMenu = renderHorizontalMenu;
4174}
4175if (typeof window.createHorizontalMenuItem === 'undefined') {
4176    window.createHorizontalMenuItem = createHorizontalMenuItem;
4177}
4178if (typeof window.createHorizontalSubMenuItem === 'undefined') {
4179    window.createHorizontalSubMenuItem = createHorizontalSubMenuItem;
4180}
4181if (typeof window.updateActiveStatesHorizontal === 'undefined') {
4182    window.updateActiveStatesHorizontal = updateActiveStatesHorizontal;
4183}
4184// Ensure toggleEditMode is globally available if it's the primary one.
4185if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder
4186    window.toggleEditMode = toggleEditMode;
4187}
4188
4189
4190// Helper function to toggle edit controls visibility
4191function toggleEditControlsVisibility(show) {
4192  const editControls = document.querySelector('.edit-controls');
4193  if (editControls) {
4194    if (show) {
4195      editControls.classList.add('visible');
4196      editControls.style.display = 'flex';
4197    } else {
4198      editControls.classList.remove('visible');
4199      editControls.style.display = 'none';
4200    }
4201  }
4202}
4203
4204/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */
4205function isHydraPlusKey(e) {
4206  if (e.repeat) return false;
4207  if (e.ctrlKey || e.metaKey || e.altKey) return false;
4208  if (e.key === '+' || e.key === 'Add') return true;
4209  if (e.code === 'NumpadAdd') return true;
4210  if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true;
4211  if (e.keyCode === 187 && e.shiftKey) return true;
4212  if (e.keyCode === 107) return true;
4213  return false;
4214}
4215
4216function handleHydraPlusShortcut() {
4217  try {
4218    const token = window.didToken || localStorage.getItem('hydra_auth_token');
4219    const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
4220    if (token && isAdmin) {
4221      console.log('User already logged in - toggling edit mode');
4222      if (typeof window.toggleEditMode === 'function') {
4223        window.toggleEditMode();
4224      } else if (typeof toggleEditMode === 'function') {
4225        toggleEditMode();
4226      } else {
4227        console.warn('toggleEditMode function not found');
4228      }
4229    } else {
4230      const p = startOTPLogin();
4231      if (p && typeof p.then === 'function') {
4232        p.catch(function(err) {
4233          console.error('startOTPLogin failed:', err);
4234        });
4235      }
4236    }
4237  } catch (err) {
4238    console.error('Hydra + key handler error:', err);
4239  }
4240}
4241
4242document.addEventListener('keydown', function(e) {
4243  if (!isHydraPlusKey(e)) return;
4244  e.preventDefault();
4245  handleHydraPlusShortcut();
4246}, true);
4247
4248// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage.
4249function isHydraCustomPageHtmlPlusMessage(e) {
4250  if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false;
4251  if (e.origin === window.location.origin) return true;
4252  // Sandboxed iframes (no allow-same-origin) post with origin "null" — accept only from our iframe.
4253  if (e.origin !== 'null' || !e.source) return false;
4254  const iframes = document.querySelectorAll('iframe.page-custom-html-iframe');
4255  for (let i = 0; i < iframes.length; i++) {
4256    if (iframes[i].contentWindow === e.source) return true;
4257  }
4258  return false;
4259}
4260
4261window.addEventListener('message', function(e) {
4262  if (!isHydraCustomPageHtmlPlusMessage(e)) return;
4263  handleHydraPlusShortcut();
4264});
4265
4266/** Option+/ (÷) opens Help → Pathways. Option+? opens Help → Ask (help admin only). */
4267function isHydraHelpKey(e) {
4268  if (e.repeat) return false;
4269  if (e.ctrlKey || e.metaKey) return false;
4270  if (!e.altKey) return false;
4271  if (e.shiftKey) return false;
4272  if (e.key === '÷') return true;
4273  if (e.code === 'Slash' && e.altKey) return true;
4274  return false;
4275}
4276
4277function isHydraHelpAskKey(e) {
4278  if (e.repeat) return false;
4279  if (e.ctrlKey || e.metaKey) return false;
4280  if (!e.altKey || !e.shiftKey) return false;
4281  if (e.key === '?' || e.key === '¿') return true;
4282  if (e.code === 'Slash') return true;
4283  return false;
4284}
4285
4286document.addEventListener('keydown', function(e) {
4287  if (!window.HYDRA_HELP_ENABLED) return;
4288  var t = e.target;
4289  if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
4290  if (isHydraHelpAskKey(e)) {
4291    e.preventDefault();
4292    e.stopPropagation();
4293    openHydraHelp({ initialTab: 'ask' });
4294    return;
4295  }
4296  if (!isHydraHelpKey(e)) return;
4297  e.preventDefault();
4298  e.stopPropagation();
4299  openHydraHelp();
4300}, true);
4301
4302// Email OTP Authentication with Enhanced Debugging
4303
4304
4305// Add this function to the client-side code
4306// Improved client-side backup function with better token handling
4307async function createLoginBackup() {
4308  try {
4309    console.log('Attempting to create login backup...');
4310    
4311    // Get authentication info from multiple possible sources
4312    let token = window.didToken || localStorage.getItem('hydra_auth_token');
4313    const email = localStorage.getItem('hydra_auth_email') || 
4314                  (window.userMetadata ? window.userMetadata.email : '') ||
4315                  '';
4316    
4317    // Check if we have authentication
4318    if (!token) {
4319      console.error('No auth token available for backup');
4320      return;
4321    }
4322    
4323    if (!email) {
4324      console.error('No email available for backup - make sure it was stored during login');
4325      return;
4326    }
4327    
4328    console.log(`Creating backup with token (${token.length} chars) for ${email}`);
4329    
4330    // Format the token correctly - VERY IMPORTANT
4331    // If it's already a hydra token (starts with hydra:), use it as is
4332    // Otherwise, add the hydra: prefix
4333    const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`;
4334    
4335    console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`);
4336    
4337    // Get the API URL with preview handling if needed
4338    const backupUrl = typeof getApiUrl === 'function' ? 
4339      getApiUrl('/api/create-backup') : 
4340      '/api/create-backup';
4341    
4342    console.log(`Sending backup request to: ${backupUrl}`);
4343    
4344    // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header
4345    const response = await fetch(backupUrl, {
4346      method: 'POST',
4347      headers: {
4348        'Authorization': `Bearer ${formattedToken}`,
4349        'Content-Type': 'application/json',
4350        'X-User-Email': email, // Include email as fallback
4351        'X-API-Request': 'true' // Mark as API request
4352      }
4353    });
4354    
4355    // Handle response
4356    if (!response.ok) {
4357      const errorText = await response.text();
4358      let errorData;
4359      try {
4360        // Try to parse as JSON
4361        errorData = JSON.parse(errorText);
4362        console.error('Backup failed:', errorData.error, errorData);
4363      } catch {
4364        // Not JSON, log raw text
4365        console.error('Backup failed with status', response.status, errorText);
4366      }
4367      return;
4368    }
4369    
4370    // Parse successful response
4371    const result = await response.json();
4372    console.log(`✅ Backup created successfully: ${result.backup}`);
4373  } catch (error) {
4374    console.error('Error in backup process:', error);
4375  }
4376}
4377
4378// Initialize Magic with detailed logging
4379function initMagic() {
4380  try {
4381    // If Magic is already initialized, return it
4382    if (window.magic) {
4383      return window.magic;
4384    }
4385    
4386    console.log('Initializing Magic SDK...');
4387    
4388    // Initialize with minimal options
4389    const magic = new Magic('pk_live_E0B68BA6DCA8C75F', {
4390      testMode: false
4391    });
4392    
4393    // Store reference globally
4394    window.magic = magic;
4395    
4396    console.log('Magic SDK initialized successfully');
4397    return magic;
4398  } catch (error) {
4399    console.error('Error initializing Magic:', error);
4400    throw error;
4401  }
4402}
4403
4404// Start OTP login flow with proper event handling
4405async function startOTPLogin() {
4406  try {
4407    // Initialize Magic
4408    const magic = initMagic();
4409    
4410    // Get email from user (styled overlay — same auth path as native prompt)
4411    const email = await showEmailLoginOverlay();
4412    if (!email) return; // User cancelled
4413    
4414    console.log('Starting Email OTP login for:', email);
4415    showLoadingOverlay('Sending verification code...');
4416    
4417    // Check if Magic's event enum types are available - for newer Magic SDK versions
4418    // If not available, fall back to string-based events
4419    const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 
4420                    typeof LoginWithEmailOTPEventEmit !== 'undefined';
4421    
4422    console.log('Using enum-based events:', useEnums);
4423    
4424    // Create a timeout handler
4425    let emailSentTimeout = setTimeout(() => {
4426      console.log('No email-otp-sent event received, using fallback UI');
4427      hideLoadingOverlay();
4428      showOTPInputOverlay(email);
4429    }, 10000);
4430    
4431    // Create OTP login handle with showUI: false
4432    const handle = magic.auth.loginWithEmailOTP({ 
4433      email, 
4434      showUI: false,
4435      deviceCheckUI: false
4436    });
4437    
4438    // Track authentication state
4439    window.pendingAuth = {
4440      email,
4441      handle
4442    };
4443    
4444    // Listen for events - using both enum-based and string-based event handling
4445    console.log('Setting up OTP event handlers...');
4446    
4447    // Email OTP sent event
4448    if (useEnums) {
4449      handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => {
4450        console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully');
4451        clearTimeout(emailSentTimeout);
4452        hideLoadingOverlay();
4453        showOTPInputOverlay(email);
4454      });
4455    }
4456    
4457    // Also listen for string-based event (for backward compatibility)
4458    handle.on('email-otp-sent', () => {
4459      console.log('EVENT: email-otp-sent - Email was sent successfully');
4460      clearTimeout(emailSentTimeout);
4461      hideLoadingOverlay();
4462      showOTPInputOverlay(email);
4463    });
4464    
4465    // Invalid OTP event
4466    if (useEnums) {
4467      handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => {
4468        console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered');
4469        showErrorMessage('Invalid verification code. Please try again.');
4470        updateOTPInputForRetry();
4471      });
4472    }
4473    
4474    // Also listen for string-based event
4475    handle.on('invalid-email-otp', () => {
4476      console.log('EVENT: invalid-email-otp - Invalid code entered');
4477      showErrorMessage('Invalid verification code. Please try again.');
4478      updateOTPInputForRetry();
4479    });
4480    
4481    async function handleLoginSuccess(result) {
4482  console.log('OTP verification successful, checking admin status...');
4483  hideOTPInputOverlay();
4484  hideLoadingOverlay();
4485
4486  try {
4487    window.didToken = result;
4488    console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none');
4489
4490    const userInfo = await magic.user.getInfo();
4491    window.userMetadata = userInfo;
4492    console.log('User info retrieved:', userInfo);
4493
4494    const isUserAdmin = await checkAdminStatus(result);
4495
4496    if (!isUserAdmin && !window.isAdmin) {
4497      console.log('User is not on admin list, login denied');
4498      showErrorMessage('Login failed');
4499      clearHydraClientAuthStorage();
4500      resetAuthUI();
4501      try {
4502        await magic.user.logout();
4503      } catch (logoutError) {
4504        console.error('Error logging out non-admin user from Magic:', logoutError);
4505      }
4506      window.pendingAuth = null;
4507      return;
4508    }
4509
4510    console.log('Admin status check completed');
4511    window.isAuthenticated = true;
4512    isAuthenticated = true;
4513    showSuccessMessage('Login successful!');
4514
4515    // Store the ID token (JWT) for API calls
4516    try {
4517      const idToken = await magic.user.getIdToken();
4518      if (idToken && idToken.includes('.')) {
4519        console.log('Got proper JWT token from Magic.link');
4520        localStorage.setItem('hydra_auth_token', idToken);
4521      } else if (userInfo && userInfo.email) {
4522        const userToken = {
4523          email: userInfo.email,
4524          iat: Math.floor(Date.now() / 1000),
4525          exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60)
4526        };
4527        const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' }));
4528        const payload = btoa(JSON.stringify(userToken));
4529        const jwtToken = header + '.' + payload + '.nosignature';
4530        localStorage.setItem('hydra_auth_token', jwtToken);
4531        localStorage.setItem('hydra_auth_email', userInfo.email);
4532      }
4533      localStorage.setItem('hydra_did_token', result);
4534    } catch (tokenError) {
4535      console.error('Error getting ID token:', tokenError);
4536      localStorage.setItem('hydra_auth_token', result);
4537    }
4538
4539    // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy
4540
4541    // 1. Add state classes to body
4542    document.body.classList.add('hydra-authenticated');
4543    if (window.isAdmin) {
4544      document.body.classList.add('hydra-admin');
4545    }
4546
4547    // 2. Direct DOM manipulation
4548    const logoutButton = document.getElementById('logoutButton');
4549    const editButton = document.getElementById('editButton');
4550
4551    // Create new buttons if they don't exist
4552    if (!logoutButton) {
4553      console.log('Creating logout button');
4554      const newLogoutButton = document.createElement('button');
4555      newLogoutButton.id = 'logoutButton';
4556      newLogoutButton.className = 'btn';
4557      newLogoutButton.textContent = 'Logout';
4558      newLogoutButton.onclick = logout;
4559      newLogoutButton.style.display = 'block';
4560
4561      const sidebarHeader = document.querySelector('.sidebar-header');
4562      if (sidebarHeader) {
4563        sidebarHeader.appendChild(newLogoutButton);
4564      } else {
4565        document.body.appendChild(newLogoutButton);
4566      }
4567    } else {
4568      logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4569    }
4570
4571    if (!editButton && window.isAdmin) {
4572      console.log('Creating edit button');
4573      const newEditButton = document.createElement('button');
4574      newEditButton.id = 'editButton';
4575      newEditButton.className = 'btn btn-primary';
4576      newEditButton.innerHTML = `
4577        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4578          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
4579          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
4580        </svg>
4581        Edit
4582      `;
4583      newEditButton.onclick = toggleEditMode;
4584      newEditButton.style.display = 'block';
4585
4586      const sidebarHeader = document.querySelector('.sidebar-header');
4587      if (sidebarHeader) {
4588        sidebarHeader.appendChild(newEditButton);
4589      } else {
4590        document.body.appendChild(newEditButton);
4591      }
4592    } else if (editButton && window.isAdmin) {
4593      editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4594    }
4595
4596    // 3. Dispatch event for other listeners
4597    document.dispatchEvent(new CustomEvent('login-complete', {
4598      detail: {
4599        isAdmin: window.isAdmin,
4600        email: window.userMetadata ? window.userMetadata.email : null
4601      }
4602    }));
4603
4604    window.pendingAuth = null;
4605
4606    if (typeof checkAuth === 'function') {
4607      console.log('Running existing checkAuth function');
4608      setTimeout(checkAuth, 500);
4609    }
4610  } catch (error) {
4611    console.error('Error in login success handling:', error);
4612    showErrorMessage('Login failed');
4613    clearHydraClientAuthStorage();
4614    resetAuthUI();
4615    window.pendingAuth = null;
4616  }
4617}
4618
4619
4620    // Completion event
4621    handle.on('done', async (result) => {
4622      await handleLoginSuccess(result);
4623    });
4624    
4625    // Error event
4626    handle.on('error', (error) => {
4627      console.error('EVENT: error - OTP login error:', error);
4628      hideLoadingOverlay();
4629      hideOTPInputOverlay();
4630      showErrorMessage('Login failed: ' + (error.message || 'Unknown error'));
4631      
4632      // Clear timeout if still active
4633      clearTimeout(emailSentTimeout);
4634      
4635      // Clean up
4636      window.pendingAuth = null;
4637    });
4638    
4639    console.log('OTP event handlers set up successfully');
4640    
4641  } catch (error) {
4642    console.error('Error starting OTP login:', error);
4643    hideLoadingOverlay();
4644    hideOTPInputOverlay();
4645    showErrorMessage('Error starting login: ' + error.message);
4646  }
4647}
4648
4649// Submit OTP code with support for enum-based events
4650function submitOTP(code) {
4651  try {
4652    // Validate code input
4653    if (!code || code.trim() === '') {
4654      showErrorMessage('Please enter the verification code.');
4655      return;
4656    }
4657    
4658    // Check if we have a pending authentication
4659    if (!window.pendingAuth || !window.pendingAuth.handle) {
4660      showErrorMessage('No active login session. Please try again.');
4661      hideOTPInputOverlay();
4662      return;
4663    }
4664    
4665    console.log('Submitting OTP code...');
4666    showLoadingOverlay('Verifying code...');
4667    
4668    // Check if enum types are available
4669    const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4670    
4671    // Verify the OTP code
4672    if (useEnums) {
4673      window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code);
4674    } else {
4675      window.pendingAuth.handle.emit('verify-email-otp', code);
4676    }
4677    
4678  } catch (error) {
4679    console.error('Error submitting OTP:', error);
4680    hideLoadingOverlay();
4681    showErrorMessage('Error verifying code: ' + error.message);
4682  }
4683}
4684
4685// Cancel OTP login with support for enum-based events
4686function cancelOTPLogin() {
4687  try {
4688    // Check if we have a pending authentication
4689    if (window.pendingAuth && window.pendingAuth.handle) {
4690      // Check if enum types are available
4691      const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4692      
4693      // Emit cancel event
4694      if (useEnums) {
4695        window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel);
4696      } else {
4697        window.pendingAuth.handle.emit('cancel');
4698      }
4699      
4700      console.log('OTP login cancelled');
4701    }
4702    
4703    // Clean up
4704    window.pendingAuth = null;
4705    
4706    // Hide any overlays
4707    hideEmailLoginOverlay();
4708    hideOTPInputOverlay();
4709    hideLoadingOverlay();
4710  } catch (error) {
4711    console.error('Error cancelling OTP login:', error);
4712  }
4713}
4714
4715// Update OTP input for retry
4716function updateOTPInputForRetry() {
4717  const otpInput = document.getElementById('otp-input');
4718  if (otpInput) {
4719    otpInput.value = '';
4720    otpInput.focus();
4721    
4722    // Add a shake animation for visual feedback
4723    otpInput.classList.add('shake');
4724    
4725    // Remove the animation class after it completes
4726    setTimeout(() => {
4727      otpInput.classList.remove('shake');
4728    }, 500);
4729  }
4730  
4731  // Hide loading overlay if it's visible
4732  hideLoadingOverlay();
4733}
4734
4735function isValidLoginEmail(value) {
4736  const trimmed = String(value || '').trim();
4737  if (!trimmed || trimmed.indexOf(' ') >= 0) return false;
4738  const at = trimmed.indexOf('@');
4739  if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false;
4740  const local = trimmed.slice(0, at);
4741  const domain = trimmed.slice(at + 1);
4742  if (!local || !domain || domain.indexOf('.') < 1) return false;
4743  if (domain.startsWith('.') || domain.endsWith('.')) return false;
4744  return true;
4745}
4746
4747function showEmailLoginOverlay() {
4748  hideEmailLoginOverlay();
4749
4750  return new Promise((resolve) => {
4751    const overlay = document.createElement('div');
4752    overlay.id = 'email-login-overlay';
4753    overlay.className = 'pe-auth-overlay';
4754
4755    const content = document.createElement('div');
4756    content.className = 'pe-auth-shell';
4757
4758    const logo = document.createElement('img');
4759    logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4760    logo.alt = 'Neon Sky Logo';
4761    logo.className = 'pe-auth-shell__logo';
4762
4763    const description = document.createElement('p');
4764    description.className = 'pe-auth-shell__desc';
4765    description.textContent = 'Enter your email address to receive a verification code.';
4766
4767    const form = document.createElement('form');
4768    form.noValidate = true;
4769    form.onsubmit = function(e) {
4770      e.preventDefault();
4771      const raw = input.value.trim();
4772      if (!isValidLoginEmail(raw)) {
4773        input.classList.add('shake');
4774        setTimeout(() => input.classList.remove('shake'), 500);
4775        return;
4776      }
4777      hideEmailLoginOverlay();
4778      resolve(raw);
4779    };
4780
4781    const input = document.createElement('input');
4782    input.id = 'email-login-input';
4783    input.type = 'email';
4784    input.name = 'email';
4785    input.placeholder = '[email protected]';
4786    input.autocomplete = 'email';
4787    input.inputMode = 'email';
4788    input.required = true;
4789    input.className = 'pe-auth-shell__input pe-auth-shell__input--email';
4790    const savedEmail = localStorage.getItem('hydra_auth_email');
4791    if (savedEmail && isValidLoginEmail(savedEmail)) {
4792      input.value = savedEmail.trim();
4793    }
4794    form.appendChild(input);
4795
4796    const buttonContainer = document.createElement('div');
4797    buttonContainer.className = 'pe-auth-actions';
4798
4799    const continueButton = document.createElement('button');
4800    continueButton.type = 'submit';
4801    continueButton.textContent = 'Continue';
4802    continueButton.className = 'pe-btn pe-btn--primary';
4803    buttonContainer.appendChild(continueButton);
4804
4805    const cancelButton = document.createElement('button');
4806    cancelButton.type = 'button';
4807    cancelButton.textContent = 'Cancel';
4808    cancelButton.className = 'pe-btn';
4809    cancelButton.onclick = function() {
4810      hideEmailLoginOverlay();
4811      resolve(null);
4812    };
4813    buttonContainer.appendChild(cancelButton);
4814    form.appendChild(buttonContainer);
4815
4816    content.appendChild(logo);
4817    content.appendChild(description);
4818    content.appendChild(form);
4819    overlay.appendChild(content);
4820    document.body.appendChild(overlay);
4821
4822    setTimeout(() => {
4823      input.focus();
4824      if (input.value) {
4825        input.select();
4826      }
4827    }, 100);
4828  });
4829}
4830
4831function hideEmailLoginOverlay() {
4832  const overlay = document.getElementById('email-login-overlay');
4833  if (overlay && overlay.parentNode) {
4834    overlay.parentNode.removeChild(overlay);
4835  }
4836}
4837
4838// Show OTP input overlay - improved version
4839function showOTPInputOverlay(email) {
4840  hideOTPInputOverlay();
4841  hideEmailLoginOverlay();
4842
4843  const overlay = document.createElement('div');
4844  overlay.id = 'otp-input-overlay';
4845  overlay.className = 'pe-auth-overlay';
4846
4847  const content = document.createElement('div');
4848  content.className = 'pe-auth-shell';
4849
4850  const logo = document.createElement('img');
4851  logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4852  logo.alt = 'Neon Sky Logo';
4853  logo.className = 'pe-auth-shell__logo';
4854
4855  const description = document.createElement('p');
4856  description.className = 'pe-auth-shell__desc';
4857  const descPrefix = document.createTextNode("We've sent a verification code to ");
4858  const descEmail = document.createElement('strong');
4859  descEmail.textContent = String(email || '');
4860  const descSuffix = document.createTextNode('. Please check your email and enter the code below:');
4861  description.appendChild(descPrefix);
4862  description.appendChild(descEmail);
4863  description.appendChild(document.createElement('br'));
4864  description.appendChild(descSuffix);
4865
4866  const form = document.createElement('form');
4867  form.onsubmit = function(e) {
4868    e.preventDefault();
4869    const otpInput = document.getElementById('otp-input');
4870    if (otpInput && otpInput.value) {
4871      submitOTP(otpInput.value);
4872    }
4873  };
4874
4875  const input = document.createElement('input');
4876  input.id = 'otp-input';
4877  input.type = 'text';
4878  input.placeholder = 'Enter code';
4879  input.pattern = '[0-9]*';
4880  input.inputMode = 'numeric';
4881  input.autocomplete = 'one-time-code';
4882  input.className = 'pe-auth-shell__input';
4883  form.appendChild(input);
4884
4885  const buttonContainer = document.createElement('div');
4886  buttonContainer.className = 'pe-auth-actions';
4887
4888  const verifyButton = document.createElement('button');
4889  verifyButton.type = 'submit';
4890  verifyButton.textContent = 'Verify';
4891  verifyButton.className = 'pe-btn pe-btn--primary';
4892  buttonContainer.appendChild(verifyButton);
4893
4894  const cancelButton = document.createElement('button');
4895  cancelButton.type = 'button';
4896  cancelButton.textContent = 'Cancel';
4897  cancelButton.className = 'pe-btn';
4898  cancelButton.onclick = function() {
4899    cancelOTPLogin();
4900  };
4901  buttonContainer.appendChild(cancelButton);
4902  form.appendChild(buttonContainer);
4903
4904  const resendContainer = document.createElement('div');
4905  resendContainer.className = 'pe-auth-resend';
4906
4907  const resendText = document.createElement('span');
4908  resendText.textContent = "Didn't receive the code? ";
4909
4910  const resendLink = document.createElement('a');
4911  resendLink.textContent = 'Resend';
4912  resendLink.href = '#';
4913  resendLink.onclick = function(e) {
4914    e.preventDefault();
4915    cancelOTPLogin();
4916    setTimeout(() => {
4917      startOTPLogin();
4918    }, 500);
4919  };
4920
4921  resendContainer.appendChild(resendText);
4922  resendContainer.appendChild(resendLink);
4923  form.appendChild(resendContainer);
4924
4925  content.appendChild(logo);
4926  content.appendChild(description);
4927  content.appendChild(form);
4928  overlay.appendChild(content);
4929  document.body.appendChild(overlay);
4930
4931  setTimeout(() => {
4932    input.focus();
4933  }, 100);
4934}
4935
4936// Hide OTP input overlay
4937function hideOTPInputOverlay() {
4938  const overlay = document.getElementById('otp-input-overlay');
4939  if (overlay && overlay.parentNode) {
4940    overlay.parentNode.removeChild(overlay);
4941  }
4942}
4943
4944// Utility function to show loading overlay
4945function showLoadingOverlay(message = 'Loading...') {
4946  hideLoadingOverlay();
4947
4948  const overlay = document.createElement('div');
4949  overlay.id = 'loading-overlay';
4950  overlay.className = 'pe-auth-overlay';
4951
4952  const content = document.createElement('div');
4953  content.className = 'pe-auth-shell';
4954
4955  const loaderContainer = document.createElement('div');
4956  loaderContainer.className = 'pe-auth-shell__loader';
4957  loaderContainer.innerHTML = `
4958    <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
4959      <defs>
4960        <filter id="svgSpinnersGooeyBalls20">
4961          <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
4962          <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"/>
4963          <feBlend in="SourceGraphic" in2="z"/>
4964        </filter>
4965      </defs>
4966      <g filter="url(#svgSpinnersGooeyBalls20)">
4967        <circle cx="5" cy="12" r="4" fill="currentColor">
4968          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
4969        </circle>
4970        <circle cx="19" cy="12" r="4" fill="currentColor">
4971          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
4972        </circle>
4973        <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
4974      </g>
4975    </svg>
4976  `;
4977  content.appendChild(loaderContainer);
4978
4979  const messageEl = document.createElement('p');
4980  messageEl.className = 'pe-auth-shell__message';
4981  messageEl.textContent = message;
4982  content.appendChild(messageEl);
4983
4984  overlay.appendChild(content);
4985  document.body.appendChild(overlay);
4986}
4987
4988// Utility function to hide loading overlay
4989function hideLoadingOverlay() {
4990  const overlay = document.getElementById('loading-overlay');
4991  if (overlay && overlay.parentNode) {
4992    overlay.parentNode.removeChild(overlay);
4993  }
4994}
4995
4996// Utility function to show success message
4997function showSuccessMessage(message, duration = 3000) {
4998  const existingToast = document.getElementById('success-toast');
4999  if (existingToast && existingToast.parentNode) {
5000    existingToast.parentNode.removeChild(existingToast);
5001  }
5002
5003  const toast = document.createElement('div');
5004  toast.id = 'success-toast';
5005  toast.className = 'pe-auth-toast';
5006  toast.textContent = message;
5007
5008  document.body.appendChild(toast);
5009
5010  setTimeout(() => {
5011    if (toast.parentNode) {
5012      toast.parentNode.removeChild(toast);
5013    }
5014  }, duration);
5015}
5016
5017// Utility function to show error message
5018function showErrorMessage(message, duration = 5000) {
5019  const existingToast = document.getElementById('error-toast');
5020  if (existingToast && existingToast.parentNode) {
5021    existingToast.parentNode.removeChild(existingToast);
5022  }
5023
5024  const toast = document.createElement('div');
5025  toast.id = 'error-toast';
5026  toast.className = 'pe-auth-toast pe-auth-toast--error';
5027  toast.textContent = message;
5028
5029  document.body.appendChild(toast);
5030
5031  setTimeout(() => {
5032    if (toast.parentNode) {
5033      toast.parentNode.removeChild(toast);
5034    }
5035  }, duration);
5036}
5037
5038// UPDATED: Check authentication status with class-based approach
5039async function checkAuth() {
5040  try {
5041    if (!magic) {
5042      console.log('Magic not initialized, skipping auth check');
5043      return;
5044    }
5045    
5046    // Prevent multiple simultaneous auth checks
5047    if (window._checkingAuth) return;
5048    window._checkingAuth = true;
5049    
5050    console.log('Checking auth status...');
5051    
5052    isAuthenticated = await magic.user.isLoggedIn();
5053    console.log('isLoggedIn check:', isAuthenticated);
5054    
5055    if (isAuthenticated) {
5056      // Add the authenticated class 
5057      document.body.classList.add('hydra-authenticated');
5058      
5059      // Get user metadata
5060      userMetadata = await magic.user.getInfo();
5061      console.log('User info:', userMetadata);
5062      
5063      // Get ID token for API calls (JWT format)
5064      const rawDidToken = await magic.user.getIdToken();
5065      console.log('ID token acquired, length:', rawDidToken.length);
5066      
5067      // Do NOT write the raw DID token to localStorage here — checkAdminStatus will
5068      // receive the server-issued hydra token and store that instead, which is what
5069      // the save function needs. Writing the DID token now would overwrite a valid
5070      // hydra token that may already be stored from the OTP login flow.
5071      
5072      // Check if user is admin; on success this updates both localStorage and didToken
5073      // with a proper hydra token via checkAdminStatus → TokenManager.storeToken
5074      await checkAdminStatus(rawDidToken);
5075    } else {
5076      // Magic has no session — still try Hydra OTP token so /_manage and return visits work
5077      const hydraOnly = localStorage.getItem('hydra_auth_token');
5078      if (hydraOnly) {
5079        try {
5080          await checkAdminStatus(hydraOnly);
5081        } catch (e) {
5082          console.error('Hydra token restore after Magic logout failed:', e);
5083        }
5084      } else {
5085        resetAuthUI();
5086      }
5087    }
5088    
5089    // Release lock
5090    window._checkingAuth = false;
5091  } catch (error) {
5092    console.error('Error checking authentication:', error);
5093    window._checkingAuth = false;
5094    resetAuthUI();
5095  }
5096}
5097
5098
5099async function checkAdminStatus(token) {
5100  try {
5101    console.log('Checking admin status...');
5102
5103    // Add user email to headers if available
5104    let userEmail = null;
5105    if (window.userMetadata && window.userMetadata.email) {
5106      userEmail = window.userMetadata.email;
5107      console.log('Using email from userMetadata:', userEmail);
5108    } else {
5109        userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage
5110        if (userEmail) console.log('Using email from localStorage:', userEmail);
5111    }
5112
5113    if (!token) {
5114      console.log('No token provided to checkAdminStatus');
5115      return false; // Indicate failure
5116    }
5117
5118    // Format the token correctly for the API call
5119    let formattedToken = token;
5120    // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix
5121    if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) {
5122        formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer
5123    } else if (token.startsWith('hydra:')) {
5124        formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens
5125    }
5126    // If it already starts with Bearer, use as is
5127
5128    // Get the API URL, handling preview mode
5129    const apiUrl = typeof getApiUrl === 'function' ?
5130      getApiUrl('/api/check-admin') :
5131      '/api/check-admin';
5132
5133    console.log('Sending admin check request to:', apiUrl);
5134
5135    // Create request headers
5136    const headers = {
5137      'Authorization': formattedToken,
5138      'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow
5139    };
5140
5141    if (userEmail) {
5142      headers['X-User-Email'] = userEmail;
5143    }
5144
5145    console.log('Admin check headers:', {
5146      'Authorization': `${formattedToken.substring(0, 20)}...`,
5147      'X-User-Email': userEmail || 'Not available'
5148    });
5149
5150    // Add timeout to the fetch request
5151    const controller = new AbortController();
5152    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
5153
5154    let data;
5155    try {
5156      const response = await fetch(apiUrl, {
5157        method: 'GET',
5158        headers: headers,
5159        signal: controller.signal
5160      });
5161
5162      clearTimeout(timeoutId); // Clear timeout if fetch succeeds
5163
5164      console.log('Admin check response status:', response.status);
5165
5166      // Get the response as text first for better debugging
5167      const responseText = await response.text();
5168      console.log('Admin check response length:', responseText.length);
5169      console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...');
5170
5171
5172      try {
5173        data = JSON.parse(responseText);
5174        console.log('Admin check data:', data);
5175      } catch (parseError) {
5176        console.error('Error parsing admin check response:', parseError);
5177        // Handle cases where the response might not be JSON (e.g., HTML error page)
5178        // For preview URLs, let's still assume admin to allow editing flow
5179        if (window.location.hostname === 'preview.neonsky.app') {
5180          console.log('Preview URL detected, assuming admin status due to non-JSON response');
5181          isAdmin = true;
5182          document.body.classList.add('hydra-admin');
5183          localStorage.setItem('hydra_is_admin', 'true');
5184          forceShowEditUI(); // Attempt to show UI
5185          return true; // Indicate potential success for preview
5186        }
5187        throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`);
5188      }
5189
5190      if (!response.ok) {
5191          // Throw an error with details from the parsed JSON if available
5192          throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`);
5193      }
5194
5195      // Process the admin status
5196      isAdmin = data.isAdmin;
5197      window.isAdmin = isAdmin; // Update global flag
5198
5199      // Update classes based on admin status
5200      if (isAdmin) {
5201        document.body.classList.add('hydra-admin');
5202        localStorage.setItem('hydra_is_admin', 'true');
5203        // Store email if we got it from the server response
5204        if (data.email) {
5205            localStorage.setItem('hydra_auth_email', data.email);
5206        } else if (userEmail) {
5207            localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used
5208        }
5209      } else {
5210        document.body.classList.remove('hydra-admin');
5211        localStorage.removeItem('hydra_is_admin');
5212        localStorage.removeItem('hydra_auth_email'); // Clear email if not admin
5213      }
5214
5215      // Update siteId if provided
5216      if (data.siteId) {
5217        siteId = data.siteId;
5218        window.siteId = siteId;
5219        if (window.Parameters) window.Parameters.siteId = siteId;
5220      }
5221
5222      // Store new token if provided (Hydra token)
5223      if (data.hydraToken) {
5224        console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...');
5225        const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken;
5226        // Always update the local didToken variable so saves immediately use the hydra token
5227        didToken = cleanHydraToken;
5228        // Store token globally and in localStorage using TokenManager if available
5229        if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
5230            window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || '');
5231        } else {
5232            // Fallback storage
5233            localStorage.setItem('hydra_auth_token', cleanHydraToken);
5234            if (data.email || userEmail) {
5235                localStorage.setItem('hydra_auth_email', data.email || userEmail);
5236            }
5237        }
5238        console.log('Stored credentials in localStorage');
5239      }
5240
5241      // Update UI based on admin status
5242      if (isAdmin) {
5243        console.log('Admin status confirmed, showing edit UI');
5244        forceShowEditUI(); // Ensure UI elements are visible
5245        // --- MODIFICATION START ---
5246        // If layout is horizontal, re-render the menu to show controls
5247        let currentLayout = 'sidebar'; // Default
5248        if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5249            currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5250        }
5251        if (currentLayout === 'horizontal') {
5252            console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.');
5253            if (typeof renderHorizontalMenu === 'function') {
5254                renderHorizontalMenu();
5255            } else {
5256                console.warn("checkAdminStatus: renderHorizontalMenu function not found.");
5257            }
5258        }
5259        // --- MODIFICATION END ---
5260        createLoginBackup(); // Create backup after successful login/admin check
5261      } else {
5262          console.log('User is not admin, resetting UI.');
5263          resetAuthUI(); // Ensure non-admin UI state
5264      }
5265
5266      return isAdmin; // Return the admin status
5267
5268    } catch (fetchError) {
5269      clearTimeout(timeoutId); // Clear timeout on error as well
5270      console.error('Fetch error during admin check:', fetchError);
5271
5272      // For preview URLs, still allow editing as a fallback
5273      if (window.location.hostname === 'preview.neonsky.app') {
5274        console.log('Preview URL + fetch error, still enabling editing');
5275        isAdmin = true;
5276        window.isAdmin = true;
5277        document.body.classList.add('hydra-admin');
5278        localStorage.setItem('hydra_is_admin', 'true');
5279        forceShowEditUI();
5280        return true; // Indicate potential success for preview
5281      }
5282
5283      // For non-preview, throw the error to indicate failure
5284      throw fetchError;
5285    }
5286  } catch (error) {
5287    console.error('Error in checkAdminStatus:', error);
5288    // Don't show alert here, let the calling function handle UI feedback
5289    // Reset UI to non-admin state on error
5290    resetAuthUI();
5291    return false; // Indicate failure
5292  }
5293}
5294
5295// UPDATED: Force showing edit UI with class-based approach
5296function forceShowEditUI() {
5297  console.log('Forcing admin/auth state for UI');
5298
5299  // Ensure global state flags are set
5300  isAuthenticated = true;
5301  isAdmin = true;
5302  window.isAuthenticated = true;
5303  window.isAdmin = true;
5304
5305  // Add admin/auth classes to body - CSS will handle button visibility
5306  document.body.classList.add('hydra-authenticated', 'hydra-admin');
5307
5308  // Store admin status in localStorage
5309  localStorage.setItem('hydra_is_admin', 'true');
5310
5311  // Show edit controls container ONLY if already in edit mode
5312  if (window.isInEditMode && window.isInEditMode()) {
5313    const editControls = document.querySelector('.edit-controls');
5314    if (editControls) {
5315      editControls.classList.add('visible');
5316      editControls.style.display = 'flex'; // Make sure container is flex
5317    }
5318    // Ensure controls inside items are visible if already editing
5319     document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5320        if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) {
5321             el.style.display = 'flex';
5322        }
5323    });
5324    
5325    // Show logout button in edit controls when in edit mode
5326    const logoutButton = document.getElementById('logoutButton');
5327    if (logoutButton) {
5328      logoutButton.style.display = 'flex';
5329      logoutButton.classList.add('visible');
5330    }
5331  } else {
5332     // Ensure edit controls container is hidden if not in edit mode
5333     const editControls = document.querySelector('.edit-controls');
5334     if (editControls) {
5335        editControls.classList.remove('visible');
5336        editControls.style.display = 'none';
5337     }
5338     
5339     // Hide logout button when not in edit mode
5340     const logoutButton = document.getElementById('logoutButton');
5341     if (logoutButton) {
5342       logoutButton.style.display = 'none';
5343       logoutButton.classList.remove('visible');
5344     }
5345  }
5346
5347  console.log('Admin/Auth classes set. CSS should handle button visibility.');
5348}
5349
5350// UPDATED: Reset auth UI with class-based approach  
5351function resetAuthUI() {
5352  document.body.classList.remove('hydra-authenticated');
5353  document.body.classList.remove('hydra-admin');
5354  
5355  
5356  const logoutButton = document.getElementById('logoutButton');
5357  if (logoutButton) {
5358    logoutButton.style.display = 'none';
5359    logoutButton.classList.remove('visible');
5360  }
5361  
5362  const editButton = document.getElementById('editButton');
5363  if (editButton) {
5364    editButton.style.display = 'none';
5365    editButton.classList.remove('visible');
5366  }
5367  
5368  // Remove admin status from localStorage
5369  localStorage.removeItem('hydra_is_admin');
5370  localStorage.removeItem('hydra_auth_token');
5371  
5372  if (isEditing) {
5373    toggleEditMode(); // Exit edit mode if active
5374  }
5375}
5376
5377// Debug function - add to your code during testing
5378function debugUIClasses() {
5379  console.log('Current body classes:', document.body.className);
5380  console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized'));
5381  console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated'));
5382  console.log('hydra-admin:', document.body.classList.contains('hydra-admin'));
5383  console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active'));
5384}
5385
5386
5387
5388async function logout() {
5389  console.log('Logout initiated');
5390  
5391  try {
5392    // Show loading indication (optional)
5393    showLoadingOverlay('Logging out...');
5394    
5395    // 1. Properly logout from Magic SDK
5396    if (window.magic) {
5397      try {
5398        await window.magic.user.logout();
5399        console.log('Magic SDK logout successful');
5400      } catch (magicError) {
5401        console.error('Error during Magic logout:', magicError);
5402        // Continue with other logout steps even if Magic SDK logout fails
5403      }
5404    }
5405    
5406    // 2. Reset all authentication state variables
5407    clearHydraClientAuthStorage();
5408    
5409    // 3. Reset all UI state - remove authentication classes from body
5410    document.body.classList.remove('hydra-authenticated');
5411    document.body.classList.remove('hydra-admin');
5412    
5413    // 5. If in edit mode, exit it first
5414    if (document.body.classList.contains('edit-mode-active')) {
5415      console.log('Exiting edit mode before logout');
5416      // If toggleEditMode exists, call it to exit edit mode
5417      if (typeof toggleEditMode === 'function') {
5418        try {
5419          toggleEditMode();
5420        } catch (editError) {
5421          console.error('Error exiting edit mode:', editError);
5422        }
5423      }
5424      
5425      // Ensure edit mode class is removed regardless
5426      document.body.classList.remove('edit-mode-active');
5427    }
5428    
5429    // 6. Ensure all edit UI elements are hidden
5430    
5431    // Hide edit and logout buttons
5432    const editButton = document.getElementById('editButton');
5433    if (editButton) {
5434      editButton.classList.remove('visible');
5435      editButton.style.display = 'none';
5436    }
5437    
5438    const logoutButton = document.getElementById('logoutButton');
5439    if (logoutButton) {
5440      logoutButton.classList.remove('visible');
5441      logoutButton.style.display = 'none';
5442    }
5443    
5444    // Hide edit controls
5445    const editControls = document.querySelector('.edit-controls');
5446    if (editControls) {
5447      editControls.style.display = 'none';
5448      editControls.classList.remove('visible');
5449    }
5450    
5451    // Hide sidebar header if it should be hidden when logged out
5452    const sidebarHeader = document.querySelector('.sidebar-header');
5453    if (sidebarHeader) {
5454      sidebarHeader.style.display = 'none';
5455    }
5456    
5457    // Hide all element controls
5458    document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5459      el.style.display = 'none';
5460    });
5461    
5462    // 7. Reset editor state if applicable
5463    if (window.Parameters) {
5464      window.Parameters.isInEditor = false;
5465    }
5466    
5467    // Hide any editing forms that might be open
5468    const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm');
5469    forms.forEach(form => {
5470      form.style.display = 'none';
5471      if (form.classList.contains('visible')) {
5472        form.classList.remove('visible');
5473      }
5474    });
5475    
5476    // Remove editing class from sidebar
5477    const sidebar = document.querySelector('.sidebar');
5478    if (sidebar) {
5479      sidebar.classList.remove('editing');
5480    }
5481    
5482    // 8. Clear any page-specific state
5483    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
5484      window.PageManager.clearCurrentPage();
5485    }
5486    
5487    // 9. Notify any components that need to know about logout
5488    document.dispatchEvent(new CustomEvent('user-logout', {
5489      detail: { timestamp: Date.now() }
5490    }));
5491    
5492    // 10. Show success message
5493    hideLoadingOverlay();
5494    showSuccessMessage('Successfully logged out');
5495    
5496    console.log('Logout complete');
5497    
5498    // 11. Optional: Refresh the page after a short delay for a clean state
5499    // Uncomment the following lines if you want the page to refresh after logout
5500    /*
5501    setTimeout(() => {
5502      window.location.reload();
5503    }, 1500);
5504    */
5505    
5506  } catch (error) {
5507    console.error('Error during logout process:', error);
5508    hideLoadingOverlay();
5509    showErrorMessage('Error during logout. Please refresh the page.');
5510  }
5511}
5512
5513// Ensure the logout button is properly connected to the logout function
5514document.addEventListener('DOMContentLoaded', function() {
5515  const logoutButton = document.getElementById('logoutButton');
5516  if (logoutButton) {
5517    // Remove any existing event listeners to avoid duplicates
5518    const newButton = logoutButton.cloneNode(true);
5519    logoutButton.parentNode.replaceChild(newButton, logoutButton);
5520    
5521    // Add fresh event listener
5522    newButton.addEventListener('click', function(e) {
5523      e.preventDefault();
5524      e.stopPropagation();
5525      logout();
5526    });
5527  }
5528});
5529
5530function stopAutoAdvanceTimer() {
5531  if (window.currentAutoAdvanceTimerId) {
5532    clearTimeout(window.currentAutoAdvanceTimerId);
5533    window.currentAutoAdvanceTimerId = null; 
5534    console.log('Auto-advance timer stopped.');
5535  }
5536}
5537window.stopAutoAdvanceTimer = stopAutoAdvanceTimer;
5538
5539function beginHydraContentLoad(type, itemId, pageId) {
5540  const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1;
5541  window.__hydraContentLoadGeneration = generation;
5542  window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1;
5543  window.__hydraContentLoadOwner = {
5544    generation,
5545    type,
5546    itemId: itemId === undefined || itemId === null ? '' : String(itemId),
5547    pageId: pageId === undefined || pageId === null ? '' : String(pageId)
5548  };
5549  return generation;
5550}
5551
5552function isHydraContentLoadCurrent(generation, type, itemId) {
5553  if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) {
5554    return false;
5555  }
5556  if (type && window._activeContentType && window._activeContentType !== type) {
5557    return false;
5558  }
5559  if (itemId != null && itemId !== '' && window.activeGalleryId != null &&
5560      String(window.activeGalleryId) !== String(itemId)) {
5561    return false;
5562  }
5563  return true;
5564}
5565
5566window.beginHydraContentLoad = beginHydraContentLoad;
5567window.isHydraContentLoadCurrent = isHydraContentLoadCurrent;
5568
5569function dismissGalleryPasswordOverlay() {
5570  if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') {
5571    window.PageManager.removeGalleryPasswordOverlay();
5572    return;
5573  }
5574  document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) {
5575    if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') {
5576      overlay._hydraPasswordAbort.abort();
5577    }
5578    overlay.remove();
5579  });
5580}
5581window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay;
5582
5583function toggleEditMode() {
5584  // Prevent multiple simultaneous calls
5585  if (window._editModeToggleInProgress) {
5586    console.log('Edit mode toggle already in progress, skipping');
5587    return;
5588  }
5589  window._editModeToggleInProgress = true;
5590
5591  console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined');
5592
5593  stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance
5594
5595  // Authentication and Authorization Checks
5596  if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) {
5597    alert('You must be logged in to edit');
5598    window._editModeToggleInProgress = false;
5599    return;
5600  }
5601  if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) {
5602    window.isAdmin = true; // Synchronize global flag
5603  }
5604  if (!window.isAdmin) {
5605    alert('You must be an admin to edit');
5606    window._editModeToggleInProgress = false;
5607    return;
5608  }
5609
5610  const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false);
5611  const newEditState = !currentEditState;
5612  
5613  console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`);
5614
5615  // Apply body class for edit mode styling
5616  if (newEditState) {
5617    console.log('Entering edit mode - setting body class and theme');
5618    document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode
5619    document.body.classList.add('edit-mode-active');
5620  } else {
5621    console.log('Exiting edit mode - removing body class and theme');
5622    document.body.removeAttribute('data-theme');
5623    document.body.classList.remove('edit-mode-active');
5624  }
5625
5626  // Update the global state and dispatch event
5627  if (typeof window.updateGlobalEditState === 'function') {
5628    window.updateGlobalEditState(newEditState); 
5629  } else {
5630    window.isEditing = newEditState; // Fallback
5631    document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } }));
5632    if (!newEditState && typeof window.closeOptionsPanel === 'function') {
5633      window.closeOptionsPanel();
5634    }
5635  }
5636
5637  if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') {
5638    setTimeout(syncContentMarginToHorizontalHeader, 50);
5639    setTimeout(syncContentMarginToHorizontalHeader, 350);
5640  }
5641
5642  const sidebarEditControls = document.querySelector('.edit-controls');
5643
5644  if (newEditState) {
5645    // Entering edit mode
5646    if (sidebarEditControls) {
5647        sidebarEditControls.style.display = 'flex';
5648        sidebarEditControls.classList.add('visible');
5649    }
5650    
5651    // Show logout button in edit controls when entering edit mode
5652    const logoutButton = document.getElementById('logoutButton');
5653    if (logoutButton) {
5654      logoutButton.style.display = 'flex';
5655      logoutButton.classList.add('visible');
5656    }
5657    
5658    // Always render the tree-like menu in the sidebar for editing
5659    if (typeof window.renderGalleries === 'function') {
5660        console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.');
5661        window.renderGalleries(); // This will show all items, including those with visible:false
5662    }
5663
5664    setTimeout(() => {
5665        if (typeof window.initializeNestedSortables === 'function') {
5666            console.log('Initializing nested sortables for edit mode');
5667            window.initializeNestedSortables();
5668        }
5669        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
5670            window.SidebarManager._reinitializeNestedSortables();
5671        }
5672    }, 150);
5673
5674    // If the currently active item was invisible, reload it now that we are in edit mode.
5675    if(window.galleries && window.activeGalleryId){
5676        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5677        if (currentGalleryItem && currentGalleryItem.visible === false) {
5678            console.log('toggleEditMode (Edit): Active item was invisible, reloading it.');
5679            if (currentGalleryItem.isPage && window.loadPage) {
5680                window.loadPage(window.activeGalleryId);
5681            } else if(window.loadGallery) {
5682                window.loadGallery(window.activeGalleryId);
5683            }
5684        }
5685    }
5686    if(typeof window.showStyleEditor === 'function' &&
5687        !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) {
5688      window.showStyleEditor(false); // Ensure style editor is hidden initially
5689    }
5690
5691  } else {
5692    // Exiting edit mode (going to Live view)
5693    if (sidebarEditControls) {
5694        sidebarEditControls.style.display = 'none';
5695        sidebarEditControls.classList.remove('visible');
5696    }
5697    
5698    // Hide logout button when exiting edit mode
5699    const logoutButton = document.getElementById('logoutButton');
5700    if (logoutButton) {
5701      logoutButton.style.display = 'none';
5702      logoutButton.classList.remove('visible');
5703    }
5704    if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') {
5705      window.MenuStyleCustomizer._prepareOverlayForLiveView();
5706    }
5707    if(typeof window.closeAllForms === 'function') window.closeAllForms();
5708    
5709    if (typeof window.destroyNestedSortables === 'function') {
5710        window.destroyNestedSortables();
5711    } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5712        window.sortableInstances.forEach(instance => instance.destroy());
5713        window.sortableInstances = [];
5714    }
5715
5716    // Re-render the menu based on the actual view mode layout.
5717    // These functions should internally handle not showing items with visible:false.
5718    let currentLayout = 'sidebar';
5719    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5720        currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5721    }
5722    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
5723        window.renderHorizontalMenu();
5724    } else if (typeof window.renderGalleries === 'function') {
5725        window.renderGalleries();
5726    }
5727
5728    // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view
5729    if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') {
5730      window.SidebarManager._syncWithGlobalEditState();
5731      window.SidebarManager.renderElements();
5732    }
5733
5734    // Handle the currently active gallery/page.
5735    if (window.galleries && window.activeGalleryId) {
5736        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5737        if (currentGalleryItem) {
5738            // The content for currentGalleryItem should remain loaded,
5739            // even if currentGalleryItem.visible is false.
5740            // The menu rendering above will hide it from the list if it's invisible.
5741            
5742            // Apply page-specific menu hiding rules.
5743            const bodyEl = document.body;
5744            // When exiting edit mode, isInEditMode() will effectively be false for this check.
5745            if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 
5746                bodyEl.classList.add('menu-hidden-on-page');
5747            } else {
5748                bodyEl.classList.remove('menu-hidden-on-page');
5749            }
5750            applyMenuOverlayClassForGallery(currentGalleryItem);
5751            // Ensure active states in the (potentially now filtered) menu are updated.
5752            // If the active item is invisible, it won't be marked active in the menu, which is fine.
5753            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5754            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5755
5756        } else {
5757            // activeGalleryId points to a non-existent item, so clear it and the content.
5758            console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.');
5759            window.activeGalleryId = null;
5760            const galleryContainer = document.querySelector('.gallery-container');
5761            if (galleryContainer) galleryContainer.innerHTML = '';
5762            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5763            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5764        }
5765    }
5766    
5767    // If, after all that, no content is effectively active (e.g., initial load to root, 
5768    // or activeGalleryId became null because the item was deleted)
5769    // then try to load a default page (home page or first visible gallery).
5770    const galleryContainer = document.querySelector('.gallery-container');
5771    // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted)
5772    const noActiveContent = window.activeGalleryId === null || 
5773                            (galleryContainer && galleryContainer.innerHTML.trim() === '');
5774
5775    if (noActiveContent && window.galleries) {
5776      console.log("toggleEditMode (Live): No active content, attempting to load default page.");
5777      const homePage = window.galleries.find(g => g.isHomePage === true);
5778      let loadedDefault = false;
5779      if (homePage) {
5780          // Load home page regardless of its visibility status for this default loading logic
5781          console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`);
5782          if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; }
5783          else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; }
5784      }
5785      
5786      if (!loadedDefault) {
5787          // If no home page, load the first *visible* gallery/page in live view.
5788          const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
5789          if (firstVisibleGallery) {
5790            console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`);
5791            if (firstVisibleGallery.isPage && window.loadPage) {
5792                window.loadPage(firstVisibleGallery.id);
5793            } else if(window.loadGallery) {
5794                window.loadGallery(firstVisibleGallery.id);
5795            }
5796          } else {
5797            console.log('toggleEditMode (Live): No home page and no visible items to load.');
5798          }
5799      }
5800    }
5801
5802    if (window.MenuStyleCustomizer &&
5803        window.MenuStyleCustomizer._overlayOpen &&
5804        typeof window.MenuStyleCustomizer.applyStyles === 'function') {
5805      window.MenuStyleCustomizer.applyStyles();
5806    }
5807  }
5808  window._editModeToggleInProgress = false;
5809}
5810
5811
5812
5813/**
5814 * Improved toggleSidebarElementForm - Closes other forms first
5815 */
5816function toggleSidebarElementForm() {
5817  const formId = 'sidebarElementForm';
5818  const form = document.getElementById(formId);
5819  
5820  if (!form) return;
5821  
5822  // If this form is already open, just close everything
5823  if (window.currentOpenForm === formId) {
5824    closeAllForms();
5825    return;
5826  }
5827  
5828  // Otherwise close all forms and open this one
5829  closeAllForms();
5830  
5831  // Show the form
5832  form.style.display = 'block';
5833  form.classList.add('visible');
5834  
5835  // Set this as the current open form
5836  window.currentOpenForm = formId;
5837}
5838
5839// New function to add a sidebar element
5840function addSidebarElement() {
5841const typeInput = document.querySelector('input[name="sidebarElementType"]:checked');
5842if (!typeInput) {
5843alert('Please select an element type');
5844return;
5845}
5846
5847const type = typeInput.value;
5848
5849// Use the SidebarManager to add the element
5850if (window.SidebarManager) {
5851const newElementId = window.SidebarManager.addElement(type);
5852toggleSidebarElementForm();
5853
5854if (newElementId) {     
5855  // Save the changes to KV store
5856  saveGalleries().then(() => {
5857    console.log(`New ${type} element added successfully with ID: ${newElementId}`);
5858  }).catch(error => {
5859    console.error('Error saving new element:', error);
5860  });
5861}
5862} else {
5863alert('SidebarManager not available');
5864}
5865}
5866
5867
5868// Global variable to store metadata
5869window.siteMetadata = {
5870  title: '',
5871  description: '',
5872  googleAnalytics: '',
5873  noIndex: false,
5874  favicon: '',
5875  ogImageUrl: '',
5876  contentLanguage: 'en',  // Default to English
5877  copyrightEnabled: false,
5878  copyrightText: '',
5879  customPublicSiteEnabled: false,
5880  customPublicSiteHtmlR2Key: '',
5881  customPublicSiteHtmlFilename: '',
5882  siteWideBasicAuth: undefined
5883};
5884
5885// Global variable to store uploaded favicon URL temporarily
5886window.uploadedFaviconUrl = '';
5887
5888function updateMetadataCopyrightFieldState() {
5889  const toggle = document.getElementById('metadataCopyrightEnabled');
5890  const textInput = document.getElementById('metadataCopyrightText');
5891  const isEnabled = !!(toggle && toggle.checked);
5892
5893  if (textInput) {
5894    textInput.disabled = !isEnabled;
5895  }
5896
5897  // Intentionally leave the surrounding form group in place; the disabled attribute on the input
5898  // provides the visual cue in PicoCSS.
5899}
5900
5901/**
5902 * Improved toggleMetadataEditor - Closes other forms first
5903 */
5904function toggleMetadataEditor() {
5905  const formId = 'metadataEditor';
5906  const editor = document.getElementById(formId);
5907  
5908  if (!editor) return;
5909  
5910  // If this form is already open, just close everything
5911  if (window.currentOpenForm === formId) {
5912    closeAllForms();
5913    return;
5914  }
5915  
5916  // Otherwise close all forms and open this one
5917  closeAllForms();
5918  
5919  // Load current values before showing
5920  loadMetadataValues();
5921  editor.style.display = 'block';
5922  
5923  // Set this as the current open form
5924  window.currentOpenForm = formId;
5925  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
5926    syncCustomPublicSiteBypassVisibility();
5927  }
5928}
5929
5930
5931// Load metadata values into the form
5932function loadMetadataValues() {
5933  // Get the current values from the global object
5934  const metadata = window.siteMetadata || {};
5935  console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata));
5936  console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics);
5937  
5938  console.warn('=== FAVICON LOAD DEBUG ===');
5939  console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata));
5940  console.warn('metadata object:', JSON.stringify(metadata));
5941  console.warn('metadata.favicon value:', metadata.favicon);
5942  console.warn('metadata.favicon type:', typeof metadata.favicon);
5943  
5944  // Populate form fields
5945  document.getElementById('metaTitle').value = metadata.title || '';
5946  document.getElementById('metaDescription').value = metadata.description || '';
5947  document.getElementById('googleAnalytics').value = metadata.googleAnalytics || '';
5948  document.getElementById('noIndexToggle').checked = metadata.noIndex || false;
5949
5950  // Schema / Identity fields
5951  const schemaType = metadata.schemaType || '';
5952  const personRadio = document.getElementById('schemaTypePerson');
5953  const orgRadio = document.getElementById('schemaTypeOrganization');
5954  if (personRadio) personRadio.checked = schemaType === 'Person';
5955  if (orgRadio) orgRadio.checked = schemaType === 'Organization';
5956  const jobTitleEl = document.getElementById('schemaJobTitle');
5957  if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || '';
5958  const cityEl = document.getElementById('schemaCity');
5959  if (cityEl) cityEl.value = metadata.schemaCity || '';
5960  const emailEl = document.getElementById('schemaEmail');
5961  if (emailEl) emailEl.value = metadata.schemaEmail || '';
5962  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
5963  if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || '';
5964  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
5965  if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || '';
5966  if (typeof updateSchemaFields === 'function') updateSchemaFields();
5967
5968  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
5969  const copyrightInput = document.getElementById('metadataCopyrightText');
5970  if (copyrightToggle) {
5971    const enabled = !!metadata.copyrightEnabled;
5972    copyrightToggle.checked = enabled;
5973    if (copyrightInput) {
5974      copyrightInput.value = metadata.copyrightText || '';
5975    }
5976  }
5977  updateMetadataCopyrightFieldState();
5978
5979  const customPubToggle = document.getElementById('customPublicSiteEnabled');
5980  if (customPubToggle) {
5981    customPubToggle.checked = !!metadata.customPublicSiteEnabled;
5982  }
5983  const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key');
5984  if (customPubKeyEl) {
5985    customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || '';
5986  }
5987  const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel');
5988  if (customPubFileLabel) {
5989    customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename
5990      ? ('Current file: ' + metadata.customPublicSiteHtmlFilename)
5991      : 'No HTML file uploaded yet.';
5992  }
5993
5994  const swAuth = metadata.siteWideBasicAuth;
5995  const swEnabled = !!(swAuth && swAuth.username && swAuth.password);
5996  const swEnableEl = document.getElementById('siteWideBasicAuthEnabled');
5997  if (swEnableEl) swEnableEl.checked = swEnabled;
5998  const swUserEl = document.getElementById('siteWideBasicAuthUsername');
5999  if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : '';
6000  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6001  if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : '';
6002  const swRealmEl = document.getElementById('siteWideBasicAuthRealm');
6003  if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : '';
6004  if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState();
6005  
6006  // Set content language dropdown (defaults to 'en')
6007  const contentLanguageSelect = document.getElementById('contentLanguage');
6008  if (contentLanguageSelect) {
6009    contentLanguageSelect.value = metadata.contentLanguage || 'en';
6010    console.log('Loaded content language:', contentLanguageSelect.value);
6011  }
6012  
6013  const faviconInput = document.getElementById('faviconUrl');
6014  if (faviconInput) {
6015    // Check if we have a recently uploaded favicon URL
6016    const faviconValue = metadata.favicon || window.uploadedFaviconUrl || '';
6017    faviconInput.value = faviconValue;
6018    console.warn('Set favicon input value to:', faviconInput.value);
6019    console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl);
6020  } else {
6021    console.error('Favicon input element not found!');
6022  }
6023  
6024  // Show favicon preview if there's an existing favicon
6025  if (metadata.favicon) {
6026    console.warn('Showing favicon preview for:', metadata.favicon);
6027    showFaviconPreview(metadata.favicon);
6028  } else {
6029    console.warn('No favicon to preview');
6030  }
6031
6032  const ogInput = document.getElementById('ogImageUrl');
6033  if (ogInput) {
6034    const ogVal = (metadata.ogImageUrl || '').trim();
6035    ogInput.value = ogVal;
6036  }
6037  const ogBtnText = document.getElementById('uploadOgImageBtnText');
6038  if (ogBtnText) {
6039    ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image';
6040  }
6041  if ((metadata.ogImageUrl || '').trim()) {
6042    showOgImagePreview(metadata.ogImageUrl.trim());
6043  } else {
6044    const ogCont = document.getElementById('ogImagePreviewContainer');
6045    if (ogCont) ogCont.innerHTML = '';
6046  }
6047  
6048  console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value);
6049}
6050
6051// Save metadata values
6052function saveMetadata() {
6053  // Get values from form
6054  const googleAnalyticsElement = document.getElementById('googleAnalytics');
6055  console.log('Google Analytics element found:', !!googleAnalyticsElement);
6056  console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND');
6057  
6058  const faviconInput = document.getElementById('faviconUrl');
6059  const faviconValue = faviconInput ? faviconInput.value : '';
6060  const ogImageInput = document.getElementById('ogImageUrl');
6061  const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : '';
6062  
6063  const contentLanguageSelect = document.getElementById('contentLanguage');
6064  const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en';
6065
6066  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
6067  const copyrightInput = document.getElementById('metadataCopyrightText');
6068  const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false;
6069  const copyrightText = copyrightInput ? copyrightInput.value.trim() : '';
6070  
6071  console.warn('=== FAVICON SAVE DEBUG ===');
6072  console.warn('Favicon input element exists:', !!faviconInput);
6073  console.warn('Favicon input value:', faviconValue);
6074  console.warn('Favicon input value length:', faviconValue.length);
6075  
6076  console.warn('=== CONTENT LANGUAGE SAVE ===');
6077  console.warn('Content language selected:', contentLanguageValue);
6078  
6079  const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : '';
6080  // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope)
6081  let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : '';
6082  if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) {
6083    const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i);
6084    gaId = match ? match[1] : '';
6085  }
6086  const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key');
6087  const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore');
6088  const schemaPersonRadio = document.getElementById('schemaTypePerson');
6089  const schemaOrgRadio = document.getElementById('schemaTypeOrganization');
6090  const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person'
6091    : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization'
6092    : '';
6093  const schemaJobTitleEl = document.getElementById('schemaJobTitle');
6094  const schemaCityEl = document.getElementById('schemaCity');
6095  const schemaEmailEl = document.getElementById('schemaEmail');
6096  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
6097  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
6098  const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : '';
6099  const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : '';
6100
6101  const normalizePublicAbsoluteUrl = (value) => {
6102    if (!value) return '';
6103    try {
6104      const parsed = new URL(value);
6105      if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return '';
6106      return parsed.href;
6107    } catch {
6108      return '';
6109    }
6110  };
6111  const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw);
6112  const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw);
6113  if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) {
6114    alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.');
6115    return;
6116  }
6117
6118  const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked);
6119  const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : '';
6120  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6121  const swPass = swPassEl ? swPassEl.value : '';
6122  const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : '';
6123
6124  if (siteWideBasicAuthEnabled && (!swUser || !swPass)) {
6125    alert('Site-wide password: enter both username and password, or turn off the option.');
6126    return;
6127  }
6128
6129  const metadata = {
6130    title: document.getElementById('metaTitle').value,
6131    description: document.getElementById('metaDescription').value,
6132    googleAnalytics: gaId,
6133    noIndex: document.getElementById('noIndexToggle').checked,
6134    favicon: faviconValue,
6135    ogImageUrl: ogImageValue,
6136    contentLanguage: contentLanguageValue,
6137    copyrightEnabled,
6138    copyrightText,
6139    customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked),
6140    customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '',
6141    customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '',
6142    schemaType: schemaType || undefined,
6143    schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '',
6144    schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
6145    schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '',
6146    ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {})
6147  };
6148
6149  if (siteWideBasicAuthEnabled && swUser && swPass) {
6150    metadata.siteWideBasicAuth = {
6151      username: swUser,
6152      password: swPass,
6153      ...(swRealm ? { realm: swRealm } : {})
6154    };
6155  }
6156  
6157  console.warn('Metadata object being created:', JSON.stringify(metadata));
6158  console.warn('Favicon in metadata object:', metadata.favicon);
6159
6160  // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet)
6161  if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) {
6162    googleAnalyticsElement.value = metadata.googleAnalytics;
6163  }
6164
6165  // Update global metadata object immediately
6166  window.siteMetadata = metadata;
6167  console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata));
6168  if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
6169    window.SidebarManager.updateMetadataFooter();
6170  }
6171
6172  // Ensure we have the latest galleries, sidebar elements, and styles
6173  // before constructing the customData object.
6174
6175  // Synchronize galleries first if the function exists
6176  if (typeof synchronizeGalleries === 'function') {
6177      synchronizeGalleries();
6178      console.log('Galleries synchronized within saveMetadata');
6179  }
6180
6181  // Retrieve current galleries (use window.galleries as the source of truth after sync)
6182  const currentGalleries = window.galleries || galleries || [];
6183  console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`);
6184
6185  // Retrieve current sidebar elements
6186  const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'
6187    ? window.SidebarManager.getElementsForSave()
6188    : null;
6189
6190  // Retrieve current menu styles
6191  const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {};
6192
6193  // Now construct customData using the retrieved current state
6194  if (window.saveGalleries) {
6195    const customData = {
6196      galleries: currentGalleries, // Use the synchronized/retrieved galleries
6197      siteMetadata: metadata, // Use the metadata gathered from the form
6198      menuStyles: currentMenuStyles // Use the retrieved menu styles
6199    };
6200    if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) {
6201      customData.sidebarElements = currentSidebarElements;
6202    }
6203  
6204
6205    console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`);
6206    console.log("Metadata being saved:", JSON.stringify(metadata));
6207    console.log("Google Analytics specifically:", metadata.googleAnalytics);
6208    console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)');
6209    console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles));
6210
6211    // Save with our correctly constructed custom data object
6212    window.saveGalleries(customData)
6213      .then(() => {
6214        alert('Metadata saved successfully');
6215        toggleMetadataEditor(); // Hide the editor
6216      })
6217      .catch(error => {
6218        console.error('Error saving metadata:', error);
6219        alert('Error saving metadata. Please try again.');
6220      });
6221  } else {
6222    alert('Save function not available');
6223  }
6224}
6225
6226  
6227// Function to add new entries to the style editor
6228function showStyleEditor(display = false) {
6229  if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') {
6230    window.MenuStyleCustomizer.openOverlay();
6231    window.currentOpenForm = 'menuStyleEditor';
6232    return;
6233  }
6234  hideStyleEditor();
6235}
6236
6237// New function to hide the style editor
6238function hideStyleEditor() {
6239  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6240    window.MenuStyleCustomizer.closeOverlay();
6241  }
6242  const styleEditor = document.getElementById('menuStyleEditor');
6243  if (styleEditor) {
6244    styleEditor.style.display = 'none';
6245  }
6246}
6247
6248
6249/**
6250 * Improved toggleAddForm - Closes other forms first and ensures proper sizing
6251 */
6252function toggleAddForm() {
6253  const formId = 'addForm';
6254  const form = document.getElementById(formId);
6255  
6256  if (!form) return;
6257  
6258  // If this form is already open, just close everything
6259  if (window.currentOpenForm === formId) {
6260    closeAllForms();
6261    return;
6262  }
6263  
6264  // Otherwise close all forms and open this one
6265  closeAllForms();
6266  
6267  // Determine the appropriate max-height based on content
6268  // First check if we're showing the page form by checking the radio button
6269  const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked;
6270  
6271  // Allow extra space for the page form
6272  const maxHeight = isPageSelected ? '600px' : '500px';
6273  
6274  // Show the form with proper max height
6275  form.style.maxHeight = maxHeight;
6276  form.style.overflow = 'visible';
6276 // Ensure all content is visible
6277  form.classList.add('visible');
6278  
6279  // Also add inline styles to ensure visibility during animation
6280  form.style.padding = '15px';
6281  
6282  // Force recalculation of max-height after UI is visible
6283  setTimeout(() => {
6284    // Get actual content height + padding
6285    const contentHeight = form.scrollHeight + 30; // Add padding
6286    
6287    // Set max-height based on content with minimum of 500px
6288    form.style.maxHeight = Math.max(contentHeight, 500) + 'px';
6289    
6290    console.log(`Setting add form max height to ${form.style.maxHeight}`);
6291  }, 50);
6292  
6293  // Populate parent options
6294  const parentSelect = document.getElementById('galleryParent');
6295  if (parentSelect) {
6296    parentSelect.innerHTML = '<option value="">None (Top Level)</option>';
6297    
6298    // Add all galleries that could be parents (including submenus)
6299    galleries.forEach(gallery => {
6300      parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
6301    });
6302  }
6303  
6304  // Set this as the current open form
6305  window.currentOpenForm = formId;
6306}
6307
6308
6309// Store the currently open form ID
6310window.currentOpenForm = null;
6311
6312/**
6313 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer).
6314 * Returns true when a child was closed so callers can keep the Style Customizer open.
6315 */
6316function closeTopmostStyleCustomizerChildOverlay() {
6317  if (document.querySelector('.pe-palette-explorer-overlay')) {
6318    if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') {
6319      const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay');
6320      window.PaletteExplorer.close();
6321      if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) {
6322        return false;
6323      }
6324      return wasOpen;
6325    }
6326  }
6327  if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') &&
6328      window.MenuStyleCustomizer &&
6329      typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') {
6330    window.MenuStyleCustomizer.closeStyleHistoryOverlay();
6331    return true;
6332  }
6333  if (document.querySelector('.pe-font-explorer-overlay') &&
6334      window.MenuStyleCustomizer &&
6335      typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') {
6336    window.MenuStyleCustomizer.closeFontExplorer();
6337    return true;
6338  }
6339  if (document.querySelector('.pe-custom-code-overlay') &&
6340      window.MenuStyleCustomizer &&
6341      typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') {
6342    window.MenuStyleCustomizer.closeCustomCodeOverlay();
6343    return true;
6344  }
6345  const colorMixer = document.getElementById('colorMixerPopup');
6346  if (colorMixer && colorMixer.classList.contains('is-open')) {
6347    colorMixer.classList.remove('is-open');
6348    return true;
6349  }
6350  return false;
6351}
6352
6353/**
6354 * Close all editor forms with improved handling for add form
6355 */
6356function closeAllForms() {
6357  if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') {
6358    while (closeTopmostStyleCustomizerChildOverlay()) {
6359      // Close nested Style Customizer panels (palette, custom code, color mixer) first.
6360    }
6361  }
6362
6363  const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer &&
6364    window.MenuStyleCustomizer._overlayOpen &&
6365    window.MenuStyleCustomizer._overlayMinimized);
6366
6367  if (!keepMinimizedStyleCustomizer &&
6368      window.MenuStyleCustomizer &&
6369      typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6370    const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen;
6371    window.MenuStyleCustomizer.closeOverlay();
6372    if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) {
6373      return;
6374    }
6375  }
6376
6377  // Add Form - Special handling to ensure proper animation
6378  const addForm = document.getElementById('addForm');
6379  if (addForm) {
6380    addForm.classList.remove('visible');
6381    
6382    // Also force style reset after animation completes
6383    setTimeout(() => {
6384      if (!addForm.classList.contains('visible')) {
6385        addForm.style.maxHeight = '0';
6386        addForm.style.overflow = 'hidden';
6387        addForm.style.padding = '0 15px';
6388      }
6389    }, 350); // Set slightly longer than CSS transition
6390  }
6391  
6392  // Style Editor (do not hide the overlay host while the minimized mutate bar is live)
6393  const styleEditor = document.getElementById('menuStyleEditor');
6394  if (styleEditor && !keepMinimizedStyleCustomizer) {
6395    styleEditor.style.display = 'none';
6396  }
6397  
6398  // Metadata Editor
6399  const metadataEditor = document.getElementById('metadataEditor');
6400  if (metadataEditor) {
6401    metadataEditor.style.display = 'none';
6402  }
6403  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
6404    syncCustomPublicSiteBypassVisibility();
6405  }
6406  
6407  // Sidebar Element Form
6408  const sidebarElementForm = document.getElementById('sidebarElementForm');
6409  if (sidebarElementForm) {
6410    sidebarElementForm.style.display = 'none';
6411    sidebarElementForm.classList.remove('visible');
6412  }
6413  
6414  // Import Classic Form
6415  const importClassicForm = document.getElementById('importClassicForm');
6416  if (importClassicForm) {
6417    importClassicForm.style.display = 'none';
6418    importClassicForm.classList.remove('visible');
6419  }
6420  
6421  // Clear the current open form
6422  window.currentOpenForm = null;
6423}
6424function downloadJson() {
6425  const data = JSON.stringify(galleries, null, 2);
6426  const blob = new Blob([data], { type: 'application/json' });
6427  const url = URL.createObjectURL(blob);
6428  const a = document.createElement('a');
6429  a.href = url;
6430  a.download = 'galleries.json';
6431  document.body.appendChild(a);
6432  a.click();
6433  document.body.removeChild(a);
6434  URL.revokeObjectURL(url);
6435}
6436
6437let galleriesBackup = [];
6438
6439
6440
6441/**
6442 * Improved version of initializeNestedSortables with a better nesting approach
6443 */
6444function initializeNestedSortables() {
6445  console.log('Initializing nested sortables with improved nesting logic...');
6446
6447  // Destroy leftover instances before binding again. A second Sortable on the
6448  // same list makes items preview during drag and snap back on release.
6449  document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => {
6450    try {
6451      const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null;
6452      if (existing && typeof existing.destroy === 'function') existing.destroy();
6453    } catch (error) {
6454      console.warn('Could not destroy existing nested sortable:', error);
6455    }
6456  });
6457
6458  // Create a backup of galleries
6459  try {
6460    galleriesBackup = JSON.parse(JSON.stringify(galleries));
6461  } catch (error) {
6462    console.error('Error creating galleries backup:', error);
6463    galleriesBackup = [];
6464  }
6465
6466  // Find all nested sortable lists
6467  const nestedSortables = document.querySelectorAll('.nested-sortable');
6468  if (nestedSortables.length === 0) {
6469    console.warn('No nested sortable elements found! Check your HTML 
6469structure.');
6470    return;
6471  }
6472
6473  // Helper: compute depth based on ancestor nested lists
6474  function computeDepth(liEl) {
6475    let depth = 0;
6476    let node = liEl;
6477    while (node) {
6478      const parentOl = node.closest('ol.nested-sortable');
6479      if (!parentOl) break;
6480      const parentLi = parentOl.closest('li');
6481      if (parentLi) {
6482        depth += 1;
6483        node = parentLi;
6484      } else {
6485        break;
6486      }
6487    }
6488    // Top-level depth should be 0
6489    return Math.max(depth - 1, 0);
6490  }
6491
6492  // Helper: apply indentation styling to an li and its subtree
6493  function applyIndentationFromDepth(rootLi) {
6494    if (!rootLi) return;
6495    const applyToItem = (li) => {
6496      const depth = computeDepth(li);
6497      // Update data-nesting-level attribute
6498      li.setAttribute('data-nesting-level', depth);
6499      // Apply padding-left style
6500      const content = li.querySelector('.gallery-item-content');
6501      if (content) {
6502        content.style.paddingLeft = (depth * 16) + 'px';
6503      }
6504    };
6505    applyToItem(rootLi);
6506    rootLi.querySelectorAll('li').forEach(applyToItem);
6507  }
6508
6509  // Helper: ensure parent has proper state and nested list
6510  function ensureParentChildState(parentLi) {
6511    if (!parentLi) return null;
6512    parentLi.classList.add('has-children');
6513    let nestedList = parentLi.querySelector('ol.nested-sortable');
6514    if (!nestedList) {
6515      nestedList = document.createElement('ol');
6516      nestedList.className = 'nested-sortable';
6517      parentLi.appendChild(nestedList);
6518    }
6519    return nestedList;
6520  }
6521
6522  // Helper: clean parent if it has no children
6523  function cleanupParentIfEmpty(parentLi) {
6524    if (!parentLi) return;
6525    const nestedList = parentLi.querySelector(':scope > ol.nested-sortable');
6526    const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10);
6527    const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null;
6528    // Opted-in pages/galleries must keep an empty drop zone and chevron.
6529    if (isNavigableMenuParent(parentItem)) {
6530      parentLi.classList.add('has-children');
6531      return;
6532    }
6533    if (nestedList && nestedList.children.length === 0) {
6534      nestedList.remove();
6535      parentLi.classList.remove('has-children');
6536    }
6537  }
6538
6539  // Initialize each sortable with improved nesting settings
6540  let sortableInstances = [];
6541
6542  for (let i = 0; i < nestedSortables.length; i++) {
6543    try {
6544      const container = nestedSortables[i];
6545      
6546      // Track where the placeholder indicates the item should be placed
6547      // This helps us detect when SortableJS incorrectly auto-nests based on mouse position
6548      let intendedPlacementContainer = null;
6549      let intendedRelatedElement = null;
6550      
6551      // Track last move to prevent oscillation
6552      let lastMoveTime = 0;
6553      let lastMoveRelatedId = null;
6554      const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds)
6555      
6556      // Track if we're dragging the last item in a folder (to prevent boundary oscillation)
6557      let isLastItemInFolder = false;
6558      let draggedItemOriginalContainer = null;
6559      
6560      // Track container changes to prevent rapid switching (hysteresis)
6561      let firstPositionFixLoopActive = false; // Prevent multiple fix loops
6562      let lastContainerDecision = null; // 'inside' or 'outside'
6563      let lastContainerDecisionTime = 0;
6564      const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches
6565      
6566      const sortable = new Sortable(container, {
6567        group: 'nested',
6568        animation: 150, // Reduced animation during drag to reduce lag
6569        easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function
6570        fallbackOnBody: false, // Disable forced fallback on desktop for better performance
6571        forceFallback: false, // Use native drag on desktop
6572        
6573        // CRITICAL: These settings make nesting much easier
6574        // Higher swap threshold makes it easier to drag over items
6575        swapThreshold: 0.65, // Increased to reduce oscillation within same folder
6576        
6577        // IMPROVED NESTING SETTINGS
6578        // Used when determining if an item should be nested
6579        invertSwap: false,   // Disable to reduce oscillation
6580        direction: 'vertical', // Lock direction to vertical to reduce oscillation
6581        
6582        // Use handle for dragging
6583        handle: '.nested-sortable-handle',
6584        
6585        // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it
6586        // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both:
6587        // - Quick click → toggles folder (onclick handler fires before drag starts)
6588        // - Click and drag → drags the item (SortableJS handles the drag)
6589        // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true,
6590        // the onclick should fire quickly enough before SortableJS intercepts
6591        filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon',
6592        preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only)
6593        
6594        // Distinguish between click and drag
6595        delay: 120,
6596        delayOnTouchOnly: true,
6597        
6598        // Empty container settings
6599        emptyInsertThreshold: 10,
6600        
6601        // Add custom ghost class
6602        ghostClass: 'sortable-ghost',
6603        chosenClass: 'sortable-chosen',
6604        dragClass: 'sortable-drag',
6605        
6606        // Make sure we listen to external move events
6607        dragoverBubble: true,
6608        
6609        // Better drag preview stability
6610        scroll: true,
6611        scrollSensitivity: 100,
6612        scrollSpeed: 20,
6613        
6614        // Reduce oscillation by making placeholder movement less sensitive
6615        forceFallback: false, // Use native drag for better performance
6616        fallbackTolerance: 0, // No tolerance - use exact position
6617        
6618        /**
6619         * NESTING LOGIC: Prevent auto-nesting based on hover
6620         * Instead, rely on SortableJS's placeholder position which is accurate
6621         * We'll verify and correct placement in onEnd based on actual DOM position
6622         */
6623        onMove: function(evt, originalEvent) {
6624          const dragged = evt.dragged;
6625          const related = evt.related;
6626          
6627          if (!dragged || !related) return true;
6628          
6629          // Safety check: Ensure items have data-id attributes
6630          // This can be missing if item was just added and DOM not fully updated
6631          if (!dragged.dataset.id || !related.dataset.id) {
6632            console.warn('⚠️ [Drag] Item missing data-id attribute, aborting drag', {
6633              draggedHasId: !!dragged.dataset.id,
6634              relatedHasId: !!related.dataset.id
6635            });
6636            return false;
6637          }
6638          
6639          // Get the IDs to check for circular references
6640          const draggedId = parseInt(dragged.dataset.id);
6641          const relatedId = parseInt(related.dataset.id);
6642          
6643          // Don't allow dropping inside itself or its descendants
6644          if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) {
6645            return false;
6646          }
6647          
6648          const currentTime = Date.now();
6649          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
6650          const draggedParent = draggedItemOriginalContainer || dragged.parentElement;
6651          
6652          // Determine if we're moving inside or outside a folder
6653          // IMPORTANT: Check if related element IS the folder item itself (parent li)
6654          // When targeting first position, SortableJS might use the folder item as related
6655          const isRelatedFolderItem = related.classList && related.classList.contains('has-children');
6656          
6657          // CRITICAL: Handle empty folder case FIRST - before any other logic
6658          // When dragging into an empty folder, related is the folder item itself
6659          // This is essentially the same as targeting first position, but for an empty folder
6660          // We need to allow this immediately, bypassing all debounce and boundary checks
6661          if (isRelatedFolderItem) {
6662            const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable');
6663            const relatedIsNavigableParent = related.classList.contains('navigable-parent-item');
6664            // Navigable parents look like normal links. Only treat them as an
6665            // empty folder when the pointer is actually over the empty list.
6666            let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0);
6667            if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) {
6668              const listRect = folderNestedSortable.getBoundingClientRect();
6669              const pointerY = originalEvent.clientY;
6670              allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10;
6671            }
6672            if (allowEmptyFolderInsert) {
6673              // Empty folder detected - this is a drop into empty folder (first position)
6674              // Always allow it, bypass all debounce and boundary logic
6675              lastMoveTime = currentTime;
6676              lastMoveRelatedId = relatedId;
6677              intendedPlacementContainer = folderNestedSortable;
6678              intendedRelatedElement = null; // No related element in empty folder
6679              // Mark as moving inside folder for tracking
6680              lastContainerDecision = 'inside';
6681              lastContainerDecisionTime = currentTime;
6682              
6683              // CRITICAL: Manually move placeholder into empty folder's nested-sortable
6684              // SortableJS might not do this automatically for empty containers
6685              requestAnimationFrame(() => {
6686                const placeholder = document.querySelector('.sortable-ghost');
6687                if (placeholder && placeholder.parentElement !== folderNestedSortable) {
6688                  // Move placeholder into the empty folder
6689                  folderNestedSortable.appendChild(placeholder);
6690                }
6691                
6692                // Update ghost indentation for empty folder
6693                const ghost = document.querySelector('.sortable-ghost');
6694                if (ghost) {
6695                  const ghostContent = ghost.querySelector('.gallery-item-content');
6696                  if (ghostContent) {
6697                    // Calculate depth from parent folder
6698                    const parentFolder = folderNestedSortable.closest('li.has-children');
6699                    if (parentFolder) {
6700                      const targetDepth = computeDepth(parentFolder) + 1;
6701                      ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
6702                    }
6703                  }
6704                }
6705              });
6706              
6707              // Return true immediately to allow the move
6708              return true;
6709            }
6710          }
6711          
6712          let actualRelatedParent = related.parentElement;
6713          
6714          // If related IS the folder item, we need to get its nested sortable container
6715          if (isRelatedFolderItem) {
6716            const nestedSortable = related.querySelector('ol.nested-sortable');
6717            if (nestedSortable) {
6718              actualRelatedParent = nestedSortable;
6719            }
6720          }
6721          
6722          const relatedParent = actualRelatedParent;
6723          
6724          const isMovingInsideFolder = relatedParent && 
6725                                       relatedParent.classList.contains('nested-sortable') && 
6726                                       relatedParent !== rootContainer;
6727          const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer;
6728          const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null);
6729          
6730          // Determine if we're moving within the same folder - this is the key check
6731          // If related IS the folder item, we're still moving within the same folder
6732          let isMovingWithinSameFolder = (isMovingInsideFolder && 
6733                                          draggedParent && 
6734                                          draggedParent.classList.contains('nested-sortable') &&
6735                                          draggedParent !== rootContainer &&
6736                                          relatedParent === draggedParent) ||
6737                                          // OR: if related is the folder item itself and we're dragging from that folder
6738                                          (isRelatedFolderItem && 
6739                                           draggedParent && 
6740                                           draggedParent.classList.contains('nested-sortable') &&
6741                                           draggedParent !== rootContainer &&
6742                                           related.querySelector('ol.nested-sortable') === draggedParent);
6743          
6744          // Special case: If related is the folder item, we're definitely trying to move within that folder
6745          // This happens when targeting first position - SortableJS uses the folder item as related
6746          if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6747            const folderNestedSortable = related.querySelector('ol.nested-sortable');
6748            if (folderNestedSortable === draggedParent) {
6749              // We're dragging within the folder, and related is the folder item
6750              // This means we're targeting first position - FORCE it to be treated as same-folder move
6751              // FORCE same-folder detection - override any previous calculation
6752              isMovingWithinSameFolder = true;
6753            }
6754          }
6755          
6756          // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens
6757          // (Don't log every single move - too noisy)
6758          if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6759            const folderItems = Array.from(draggedParent.children);
6760            const draggedIndex = folderItems.indexOf(dragged);
6761            const relatedIndex = folderItems.indexOf(related);
6762            
6763            // Get placeholder element to see where it actually is
6764            const placeholder = document.querySelector('.sortable-ghost');
6765            const placeholderParent = placeholder ? placeholder.parentElement : null;
6766            const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 
6767              ? Array.from(placeholderParent.children).indexOf(placeholder) 
6768              : -1;
6769            
6770            const folderItem = draggedParent.closest('li.has-children');
6771            const folderItemId = folderItem ? folderItem.dataset.id : null;
6772            const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null;
6773            const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null;
6774            
6775            // Check if we're trying to target first position
6776            // related could be the first item OR the folder item itself (when SortableJS uses folder as related)
6777            const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder;
6778            const isRelatedFolderItemCheck = related === folderItem;
6779            const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean
6780            
6781            // When targeting first via folder item, placeholder should be at index 0
6782            // But we're seeing it at index 1 - this is the issue!
6783            const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0;
6784            
6785            // Check if placeholder is outside the folder when it should be inside
6786            const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable'));
6787            const shouldBeInside = draggedIndex >= 0; // We're dragging from inside
6788            
6789            // Only log when there's a potential issue or interesting state change
6790            const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0;
6791            const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0;
6792            const jumpingOutToRoot = placeholderIsOutside && shouldBeInside;
6793            
6794            // First position issue: trying to target first but:
6795            // - placeholder is outside OR 
6796            // - placeholder is at index 1 instead of 0 (when targeting first via folder item)
6797            const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition);
6798            
6799            // ALWAYS log when targeting first position via folder item (this is the key case we're debugging)
6800            // OR when placeholder jumps out when it should stay in
6801            // OR when placeholder should be at first position but isn't
6802            const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition;
6803            
6804            if (shouldLog) {
6805              console.error('🔍 [Placeholder Position]', {
6806                draggedId,
6807                draggedIndex,
6808                relatedId,
6809                relatedIndex,
6810                placeholderIndex,
6811                relatedElementTag: related.tagName,
6812                isRelatedFolderItem: isRelatedFolderItemCheck,
6813                isTargetingFirstViaFolderItem,
6814                isMovingWithinSameFolder,
6815                placeholderMismatch,
6816                jumpingOut,
6817                jumpingOutToRoot,
6818                isFirstPosition,
6819                placeholderIsOutside,
6820                placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'),
6821                folderItemId,
6822                firstItemInFolderId,
6823                firstPositionIssue,
6824                shouldBeAtFirstPosition,
6825                expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex
6826              });
6827            }
6828          }
6829          
6830          // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic
6831          // This allows smooth reordering just like items not in folders
6832          if (isMovingWithinSameFolder) {
6833            // Simple debounce only - same as root level items
6834            const timeSinceLastMove = currentTime - lastMoveTime;
6835            // Only prevent very rapid oscillation to same element
6836            if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) {
6837              return false;
6838            }
6839            // Allow all moves within same folder - update tracking and continue
6840            
6841            // SPECIAL FIX: When targeting first position via folder item, fix placeholder position
6842            // SortableJS places it at index 1 when related is the folder item, but we want it at index 0
6843            // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive
6844            if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6845              const folderItems = Array.from(draggedParent.children);
6846              const firstItem = folderItems.length > 0 ? folderItems[0] : null;
6847              
6848              // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up)
6849              // This prevents the fix from being too aggressive when moving to other positions
6850              const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions
6851              
6852              if (firstItem && firstItem !== dragged && isNearFirstPosition) {
6853                // Fix immediately (synchronously) first - don't wait for next frame
6854                const placeholder = document.querySelector('.sortable-ghost');
6855                if (placeholder && placeholder.parentElement === draggedParent) {
6856                  const currentIndex = Array.from(draggedParent.children).indexOf(placeholder);
6857                  // Only fix if placeholder is at index 1 when we're targeting first position
6858                  // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3)
6859                  if (currentIndex === 1 && draggedIndex <= 1) {
6860                    draggedParent.insertBefore(placeholder, firstItem);
6861                  }
6862                }
6863              } else {
6864                // Not near first position - stop any active fix loop
6865                firstPositionFixLoopActive = false;
6866              }
6867            } else {
6868              // Not targeting first position anymore - reset flag
6869              firstPositionFixLoopActive = false;
6870            }
6871            
6872            lastMoveTime = currentTime;
6873            lastMoveRelatedId = relatedId;
6874            // Skip all the boundary case logic below - just continue to allow the move
6875            // Fall through to update intendedPlacementContainer and return true
6876          } else {
6877            // Only apply complex boundary logic for moves BETWEEN folders or to/from root
6878            
6879            // Hysteresis: Prevent rapid switching between inside/outside folder
6880            const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : [];
6881            const isFirstPositionInFolder = isMovingInsideFolder && 
6882                                           targetFolderItems.length > 0 && 
6883                                           related === targetFolderItems[0];
6884            // Special case: Empty folder - allow easy dropping into it
6885            const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0;
6886            
6887            if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) {
6888              const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime;
6889              
6890              // Make first position easier to target from outside
6891              if (isFirstPositionInFolder) {
6892                // Moving to first position from outside - use shorter debounce
6893                const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3;
6894                if (timeSinceContainerSwitch < effectiveDebounce) {
6895                  // Only block if it's very rapid (oscillation)
6896                  if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) {
6897                    return false;
6898                  }
6899                }
6900              } else if (isEmptyFolder) {
6901                // Empty folder - allow easy dropping, minimal debounce
6902                // Only block very rapid oscillation
6903                if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) {
6904                  return false;
6905                }
6906              } else {
6907                // Normal container switch - apply full hysteresis
6908                if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) {
6909                  return false;
6910                }
6911              }
6912            }
6913          }
6914          
6915          // Special handling for boundary cases (last item in folder, or dragging into folder)
6916          // BUT: Skip all this logic if we're moving within the same folder (already handled above)
6917          if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6918            const folderItem = draggedParent.closest('li.has-children');
6919            const currentFolderItems = Array.from(draggedParent.children);
6920            const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 
6921                                                   currentFolderItems[0] !== dragged &&
6922                                                   (related === currentFolderItems[0] || 
6923                                                    (isMovingInsideFolder && relatedParent === draggedParent && 
6924                                                     related === currentFolderItems[0]));
6925            
6926            // Case 0: This case is now handled earlier for same-folder moves
6927            // Only handle cross-folder moves here
6928            
6929            // Case 1: Dragging last item out of folder
6930            if (isLastItemInFolder && isMovingOutsideFolder) {
6931              const timeSinceLastMove = currentTime - lastMoveTime;
6932              
6933              // BUT: If we're trying to move to first position, don't block it
6934              // Check if the related element is the folder itself or right after it
6935              if (folderItem) {
6936                const folderNextSibling = folderItem.nextElementSibling;
6937                // If we're moving to position right after folder, it might be trying to go to first position
6938                // Allow it if it's been a reasonable time
6939                if (folderNextSibling && related === folderNextSibling) {
6940                  // This is ambiguous - could be first position or outside
6941                  // Require more time but not as much as pure outside move
6942                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
6943                    if (relatedId === lastMoveRelatedId) {
6944                      return false;
6945                    }
6946                  }
6947                } else {
6948                  // Definitely moving outside - require longer debounce
6949                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
6950                    if (relatedId === lastMoveRelatedId) {
6951                      return false; // Reject if oscillating
6952                    }
6953                  }
6954                }
6955              } else {
6956                // No folder item found - apply standard debounce
6957                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
6958                  if (relatedId === lastMoveRelatedId) {
6959                    return false;
6960                  }
6961                }
6962              }
6963            }
6964            
6965            // Case 2: Dragging item into folder from outside
6966            if (isMovingInsideFolder && relatedParent !== draggedParent) {
6967              const targetFolderItems = Array.from(relatedParent.children);
6968              const isEmptyFolder = targetFolderItems.length === 0;
6969              const isNearBottom = targetFolderItems.length > 0 && 
6970                                   (related === targetFolderItems[targetFolderItems.length - 1] ||
6971                                    targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
6972              const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0];
6973              
6974              // Special handling for empty folder - allow easy dropping
6975              if (isEmptyFolder) {
6976                const timeSinceLastMove = currentTime - lastMoveTime;
6977                // Very minimal debounce for empty folder - only prevent rapid oscillation
6978                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) {
6979                  return false;
6980                }
6981                // Allow move into empty folder - don't block it
6982              } else if (isFirstPos) {
6983                // Moving to first position - make it easy
6984                const timeSinceLastMove = currentTime - lastMoveTime;
6985                // Very short debounce for first position
6986                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) {
6987                  return false;
6988                }
6989                // Allow move to first position
6990              } else if (isNearBottom) {
6991                // For bottom position, still use some debounce to prevent oscillation
6992                const timeSinceLastMove = currentTime - lastMoveTime;
6993                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) {
6994                  if (relatedId === lastMoveRelatedId) {
6995                    return false;
6996                  }
6997                }
6998              }
6999            }
7000            
7001          // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within
7002          // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last
7003          if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) {
7004            // We're dragging from within a folder but detected as moving outside
7005            // Check if the related element is adjacent to the folder (which might indicate targeting first/last)
7006            const folderIndex = Array.from(rootContainer.children).indexOf(folderItem);
7007            const relatedIndex = Array.from(rootContainer.children).indexOf(related);
7008            
7009            // If related is immediately before or after folder, might be targeting first/last
7010            if (folderIndex >= 0 && relatedIndex >= 0) {
7011              const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1);
7012              
7013              if (isAdjacentToFolder) {
7014                // Might be targeting first/last position - be lenient
7015                const timeSinceLastMove = currentTime - lastMoveTime;
7016                // Only block if it's very rapid oscillation
7017                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) {
7018                  return false;
7019                }
7020                // Otherwise allow - user might be trying to target first/last position
7021                // Don't block based on this check
7022              }
7023            }
7024          }
7025          }
7026          
7027          // Case 3: Dragging from root into folder
7028          if (draggedParent === rootContainer && isMovingInsideFolder) {
7029            const targetFolderItems = Array.from(relatedParent.children);
7030            const isEmptyFolder = targetFolderItems.length === 0;
7031            const isNearBottom = targetFolderItems.length > 0 && 
7032                                 (related === targetFolderItems[targetFolderItems.length - 1] ||
7033                                  targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
7034            const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0];
7035            
7036            // Special handling for empty folder - allow easy dropping
7037            if (isEmptyFolder) {
7038              const timeSinceLastMove = currentTime - lastMoveTime;
7039              // Very minimal debounce for empty folder - only prevent rapid oscillation
7040              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) {
7041                if (relatedId === lastMoveRelatedId) {
7042                  return false;
7043                }
7044              }
7045              // Allow move into empty folder - don't block it
7046            } else if (isFirstPosition) {
7047              // Special handling for first position - make it easier to target
7048              const timeSinceLastMove = currentTime - lastMoveTime;
7049              // Make first position easier - only prevent very rapid oscillation
7050              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) {
7051                if (relatedId === lastMoveRelatedId) {
7052                  return false;
7053                }
7054              }
7055              // Allow move to first position - don't block it
7056              // Continue to track but don't apply strict debouncing
7057            } else if (isNearBottom) {
7058              // Keep strict debouncing for bottom position
7059              const timeSinceLastMove = currentTime - lastMoveTime;
7060              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
7061                if (relatedId === lastMoveRelatedId) {
7062                  return false;
7063                }
7064              }
7065            }
7066          }
7067          
7068          // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position)
7069          // If we're at first position and trying to move outside, require deliberate movement
7070          if (isMovingInsideFolder) {
7071            const currentFolderItems = Array.from(relatedParent.children);
7072            const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0];
7073            
7074            // If we're moving FROM first position to outside (reverse hysteresis)
7075            if (isFirstPos && lastContainerDecision === 'outside') {
7076              const timeSinceLastMove = currentTime - lastMoveTime;
7077              // If we just moved outside, require more time before allowing back to first position
7078              // BUT: if it's been a reasonable time, allow it (don't make it too hard)
7079              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) {
7080                // Very rapid - might be oscillation
7081                return false;
7082              }
7083            }
7084          }
7085          
7086          // Case 5: Prevent oscillation between first and second position within folder
7087          // BUT: Only apply if NOT moving within same folder (to allow smooth reordering)
7088          if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) {
7089            const folderItems = Array.from(relatedParent.children);
7090            const isFirstPos = folderItems.length > 0 && related === folderItems[0];
7091            const isSecondPos = folderItems.length > 1 && related === folderItems[1];
7092            
7093            // If moving from first to second (or vice versa) very rapidly, prevent oscillation
7094            if ((isFirstPos || isSecondPos) && lastMoveRelatedId) {
7095              const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]');
7096              if (lastRelated && lastRelated.parentElement === relatedParent) {
7097                const lastRelatedIndex = folderItems.indexOf(lastRelated);
7098                const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0);
7099                
7100                if (isOscillating) {
7101                  const timeSinceLastMove = currentTime - lastMoveTime;
7102                  // Allow moving to first position easily, but prevent rapid oscillation between first/second
7103                  if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) {
7104                    return false; // Prevent jumping from first to second too easily
7105                  }
7106                  // But allow first position more easily
7107                  if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) {
7108                    return false; // Only prevent very rapid oscillation
7109                  }
7110                }
7111              }
7112            }
7113          }
7114          
7115          // Prevent rapid oscillation when dragging within same container
7116          // NOTE: For same-folder moves, we already updated tracking above, so skip this 
7116section
7117          if (!isMovingWithinSameFolder) {
7118            const timeSinceLastMove = currentTime - lastMoveTime;
7119            
7120            // If moving to the same related element very quickly, reject to prevent oscillation
7121            if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) {
7122              return false;
7123            }
7124            
7125            // Update tracking variables (only for non-same-folder moves)
7126            lastMoveTime = currentTime;
7127            lastMoveRelatedId = relatedId;
7128          }
7129          // For same-folder moves, tracking was already updated in the earlier block
7130          
7131          // Update container decision tracking
7132          if (currentDecision) {
7133            if (currentDecision !== lastContainerDecision) {
7134              lastContainerDecisionTime = currentTime;
7135            }
7136            lastContainerDecision = currentDecision;
7137          }
7138          
7139          // Track where the placeholder indicates the item should be placed
7140          // The related element is next to where the placeholder will be positioned
7141          // Check if it's at root level or nested level
7142          if (related) {
7143            intendedRelatedElement = related;
7144            const relatedParent = related.parentElement;
7145            const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7146            
7147            // Determine if the placeholder is at root level or nested level
7148            if (relatedParent === rootContainer) {
7149              // Placeholder is at root level (not nested)
7150              intendedPlacementContainer = rootContainer;
7151            } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) {
7152              // Placeholder is in a nested list (inside a folder)
7153              intendedPlacementContainer = relatedParent;
7154            }
7155            
7156            // Update ghost element indentation to match the target position
7157            requestAnimationFrame(() => {
7158              const ghost = document.querySelector('.sortable-ghost');
7159              if (ghost && intendedPlacementContainer) {
7160                const ghostContent = ghost.querySelector('.gallery-item-content');
7161                if (ghostContent) {
7162                  // Calculate the depth where the ghost will be placed
7163                  // Use the related element to determine the depth (if it exists and is in the target container)
7164                  let targetDepth = 0;
7165                  if (intendedPlacementContainer === rootContainer) {
7166                    targetDepth = 0; // Root level
7167                  } else if (related && related.parentElement === intendedPlacementContainer) {
7168                    // Use the related element's depth to determine target depth
7169                    targetDepth = computeDepth(related);
7170                  } else {
7171                    // Fallback: find first child in the container or calculate from parent folder
7172                    const firstChild = intendedPlacementContainer.querySelector('li');
7173                    if (firstChild) {
7174                      targetDepth = computeDepth(firstChild);
7175                    } else {
7176                      // Empty container (like empty folder) - calculate from parent folder
7177                      const parentFolder = intendedPlacementContainer.closest('li.has-children');
7178                      if (parentFolder) {
7179                        targetDepth = computeDepth(parentFolder) + 1;
7180                      }
7181                    }
7182                  }
7183                  
7184                  // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet,
7185                  // ensure placeholder is moved there and indentation is correct
7186                  if (intendedPlacementContainer.children.length === 0) {
7187                    const placeholder = document.querySelector('.sortable-ghost');
7188                    if (placeholder && placeholder.parentElement !== intendedPlacementContainer) {
7189                      // Move placeholder into empty container
7190                      intendedPlacementContainer.appendChild(placeholder);
7191                    }
7192                  }
7193                  ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
7194                }
7195              }
7196            });
7197          }
7198          
7199          // Let SortableJS handle the placement based on placeholder position
7200          // We'll verify and correct if needed in onEnd
7201          return true; // Allow the move
7202        },
7203        
7204        // Capture indentation before drag starts (fires when item is chosen)
7205        onChoose: function(evt) {
7206          if (!evt.item) return;
7207          
7208          const draggedItem = evt.item;
7209          const content = draggedItem.querySelector('.gallery-item-content');
7210          
7211          // Store the original padding-left for the ghost
7212          if (content) {
7213            const computedStyle = window.getComputedStyle(content);
7214            const originalPaddingLeft = computedStyle.paddingLeft;
7215            draggedItem.dataset.originalPaddingLeft = originalPaddingLeft;
7216          }
7217        },
7218        
7219        // Visual feedback on drag start
7220        onStart: function(evt) {
7221          if (!evt.item) return;
7222          
7223          // Don't set dimensions on the dragged item - let it maintain natural size
7224          // Setting width/height can cause layout shifts and menu expansion
7225          const draggedItem = evt.item;
7226          
7227          // Store original container and check if it's the last item in folder
7228          draggedItemOriginalContainer = draggedItem.parentElement;
7229          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7230          if (draggedItemOriginalContainer && 
7231              draggedItemOriginalContainer.classList.contains('nested-sortable') && 
7232              draggedItemOriginalContainer !== rootContainer) {
7233            const siblings = Array.from(draggedItemOriginalContainer.children);
7234            isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem;
7235          } else {
7236            isLastItemInFolder = false;
7237          }
7238          
7239          // Apply the stored padding-left to the ghost element
7240          // The ghost is created by SortableJS at this point
7241          requestAnimationFrame(() => {
7242            const ghost = document.querySelector('.sortable-ghost');
7243            if (ghost) {
7244              const ghostContent = ghost.querySelector('.gallery-item-content');
7245              const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft;
7246              if (ghostContent && originalPaddingLeft) {
7247                ghostContent.style.paddingLeft = originalPaddingLeft;
7248              }
7249            }
7250          });
7251          
7252          // Reset oscillation tracking variables
7253          lastMoveTime = 0;
7254          lastMoveRelatedId = null;
7255          lastContainerDecision = null;
7256          lastContainerDecisionTime = 0;
7257          
7258          // Create a backup
7259          try {
7260            window._dragStartElementIds = [];
7261            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7262              if (li.dataset.id) {
7263                window._dragStartElementIds.push(li.dataset.id);
7264              }
7265            });
7266            
7267            galleriesBackup = JSON.parse(JSON.stringify(galleries));
7268          } catch (error) {
7269            console.error('Error creating backup:', error);
7270          }
7271          
7272          // Add indicator classes
7273          document.body.classList.add('menu-item-dragging');
7274          
7275          // Add hover indicator class to submenu items
7276          document.querySelectorAll('.has-children').forEach(item => {
7277            item.classList.add('potential-parent');
7278          });
7279          
7280          // Prevent layout recalculation during drag
7281          document.body.style.setProperty('--is-dragging', '1');
7282        },
7283        
7284        // Cleanup on end
7285        onEnd: function(evt) {
7286          // Verify and correct placement based on placeholder's intended position
7287          // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level
7288          const dragged = evt.item;
7289          const previousParentLi = evt.from ? evt.from.closest('li') : null;
7290          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7291          
7292          if (!dragged || !rootContainer) {
7293            intendedPlacementContainer = null;
7294            intendedRelatedElement = null;
7295            // Restore normal sizing
7296            dragged.style.width = '';
7297            dragged.style.height = '';
7298            dragged.style.minHeight = '';
7299            document.body.style.removeProperty('--is-dragging');
7300            return;
7301          }
7302          
7303          // Get the actual parent container where SortableJS placed the item
7304          const actualParentList = dragged.parentElement;
7305          
7306          // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing
7307          requestAnimationFrame(() => {
7308            dragged.style.width = '';
7309            dragged.style.height = '';
7310            dragged.style.minHeight = '';
7311            document.body.style.removeProperty('--is-dragging');
7312          });
7313          
7314          if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) {
7315            intendedPlacementContainer = null;
7316            intendedRelatedElement = null;
7317          }
7318
7319          // Check if the intended placement (where placeholder showed) differs from actual placement
7320          // This happens when SortableJS auto-nests based on mouse position over a folder
7321          if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) {
7322            // The item ended up in a different container than the placeholder indicated
7323            // Move it to where the placeholder showed it should be (next to intendedRelatedElement)
7324            if (intendedPlacementContainer === rootContainer) {
7325              // Placeholder was at root level - move item to root, positioned next to related element
7326              if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) {
7327                // Insert before the related element (where placeholder was)
7328                rootContainer.insertBefore(dragged, intendedRelatedElement);
7329                console.log("Corrected placement: moved from folder to root level where placeholder indicated");
7330              } else if (intendedRelatedElement) {
7331                // Related element exists but not at root - find its folder and insert before it at root
7332                const folderItem = intendedRelatedElement.closest('li.has-children');
7333                if (folderItem && folderItem.parentElement === rootContainer) {
7334                  rootContainer.insertBefore(dragged, folderItem);
7335                  console.log("Corrected placement: moved to root level above folder where placeholder indicated");
7336                } else {
7337                  rootContainer.appendChild(dragged);
7338                }
7339              } else {
7340                rootContainer.appendChild(dragged);
7341              }
7342              // Update indentation since it's now at root
7343              applyIndentationFromDepth(dragged);
7344            } else {
7345              // Placeholder was in a nested list - move it there
7346              if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) {
7347                // Insert before the related element in the nested list
7348                intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement);
7349              } else {
7350                intendedPlacementContainer.appendChild(dragged);
7351              }
7352              console.log("Corrected placement: moved to nested list where placeholder indicated");
7353              // Update indentation for nested item
7354              applyIndentationFromDepth(dragged);
7355              
7356              // Ensure parent folder has proper structure
7357              const folderItem = intendedPlacementContainer.closest('li.has-children');
7358              if (folderItem) {
7359                const nestedList = folderItem.querySelector('ol.nested-sortable');
7360                if (nestedList && intendedPlacementContainer !== nestedList) {
7361                  nestedList.appendChild(dragged);
7362                }
7363                
7364                const parentDepth = computeDepth(folderItem);
7365                const nestedDepth = parentDepth + 1;
7366                
7367                if (nestedList) {
7368                  nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7369                }
7370                
7371                folderItem.classList.add('has-children');
7372              }
7373            }
7374          } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) {
7375            // Item is correctly in a nested list - ensure proper structure
7376            const folderItem = actualParentList.closest('li.has-children');
7377            if (folderItem) {
7378              const nestedList = folderItem.querySelector('ol.nested-sortable');
7379              if (nestedList && actualParentList !== nestedList) {
7380                nestedList.appendChild(dragged);
7381              }
7382              
7383              const parentDepth = computeDepth(folderItem);
7384              const nestedDepth = parentDepth + 1;
7385              
7386              if (nestedList) {
7387                nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7388              }
7389              
7390              folderItem.classList.add('has-children');
7391            }
7392            applyIndentationFromDepth(dragged);
7393          } else {
7394            // Item is correctly at root level
7395            applyIndentationFromDepth(dragged);
7396          }
7397
7398          // Reset tracking variables
7399          intendedPlacementContainer = null;
7400          intendedRelatedElement = null;
7401          isLastItemInFolder = false;
7402          draggedItemOriginalContainer = null;
7403          lastContainerDecision = null;
7404          lastContainerDecisionTime = 0;
7405
7406          // Apply indentation updates for ALL affected items after drag
7407          // Use requestAnimationFrame to batch DOM updates and prevent visual jumps
7408          requestAnimationFrame(() => {
7409            if (dragged) {
7410              // Update the dragged item and all its descendants
7411              applyIndentationFromDepth(dragged);
7412              
7413              // Update all items in the new container (siblings of dragged item)
7414              const newContainer = dragged.parentElement;
7415              if (newContainer && newContainer.classList.contains('nested-sortable')) {
7416                const siblings = Array.from(newContainer.children);
7417                siblings.forEach(sibling => {
7418                  if (sibling !== dragged && sibling.tagName === 'LI') {
7419                    applyIndentationFromDepth(sibling);
7420                  }
7421                });
7422              }
7423              
7424              // Update all items in the old container (if it still exists and has items)
7425              if (evt.from && evt.from.classList.contains('nested-sortable')) {
7426                const oldSiblings = Array.from(evt.from.children);
7427                oldSiblings.forEach(sibling => {
7428                  if (sibling.tagName === 'LI') {
7429                    applyIndentationFromDepth(sibling);
7430                  }
7431                });
7432              }
7433              
7434              // Also update all items in the entire tree to ensure consistency
7435              // This handles edge cases where indentation might be off
7436              const allItems = document.querySelectorAll('#galleryTree .nested-sortable li');
7437              allItems.forEach(li => {
7438                const depth = computeDepth(li);
7439                li.setAttribute('data-nesting-level', depth);
7440                const content = li.querySelector('.gallery-item-content');
7441                if (content) {
7442                  content.style.paddingLeft = (depth * 16) + 'px';
7443                }
7444              });
7445            }
7446          });
7447
7448          // Clean up previous parent if it lost its last child
7449          if (previousParentLi) {
7450            cleanupParentIfEmpty(previousParentLi);
7451          }
7452          
7453          // Remove all indicator classes
7454          document.body.classList.remove('menu-item-dragging');
7455          document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => {
7456            el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested');
7457          });
7458          
7459          // Standard cleanup for ghost elements
7460          document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => {
7461            item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag');
7462          });
7463          
7464          // Check if drop was outside a valid container
7465          if (!evt.to || !evt.to.classList.contains('nested-sortable')) {
7466            console.warn('Item was dropped outside a valid container - reverting');
7467            restoreFromBackup();
7468            return;
7469          }
7470          
7471          // Verify no elements were lost
7472          try {
7473            // Check DOM elements
7474            const endElementIds = [];
7475            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7476              if (li.dataset.id) {
7477                endElementIds.push(li.dataset.id);
7478              }
7479            });
7480            
7481            if (window._dragStartElementIds && window._dragStartElementIds.length > 0) {
7482              const startCount = window._dragStartElementIds.length;
7483              const endCount = endElementIds.length;
7484              
7485              if (endCount < startCount) {
7486                console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`);
7487                restoreFromBackup();
7488                return;
7489              }
7490            }
7491            
7492            // Update the data structure
7493            updateGalleryStructure();
7494            
7495            // Verify data model
7496            const originalCount = countGalleries(galleriesBackup);
7497            const newCount = countGalleries(galleries);
7498            
7499            if (newCount < originalCount) {
7500              console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`);
7501              restoreFromBackup();
7502              return;
7503            }
7504            
7505            // updateGalleryStructure already persists; do not save or re-render again
7506            // here. A second save/render can rebind Sortable and snap the drop back.
7507          } catch (error) {
7508            console.error('Error updating gallery structure:', error);
7509            restoreFromBackup();
7510          }
7511        }
7512      });
7513      
7514      sortableInstances.push(sortable);
7515    } catch (error) {
7516      console.error(`Error initializing sortable for container ${i}:`, error);
7517    }
7518  }
7519
7520  // Add event delegation for folder toggles (will be added by setupFolderToggleHandler)
7521  setupFolderToggleHandler();
7522
7523  return sortableInstances;
7524}
7525
7526// Helper function to set up folder toggle event delegation
7527// This must be called after galleries are rendered, in both view and edit modes
7528function setupFolderToggleHandler() {
7529  const galleryTree = document.getElementById('galleryTree');
7530  if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) {
7531    galleryTree.addEventListener('click', function(e) {
7532      
7533      const target = e.target;
7534      // Resolve the clicked row first — do not use target.closest('.menu-item.folder'),
7535      // which matches ancestor folder labels when clicking nested pages/galleries.
7536      const listItem = target.closest('li[data-id]');
7537      if (!listItem) {
7538        return;
7539      }
7540
7541      const contentDiv = listItem.querySelector(':scope > .gallery-item-content');
7542      if (!contentDiv) {
7543        return;
7544      }
7545
7546      const toggleButton = contentDiv.querySelector('.submenu-toggle');
7547      const folderText = contentDiv.querySelector('.menu-item.folder');
7548      const isToggleClick = toggleButton && toggleButton.contains(target);
7549      const isFolderTextClick = folderText && folderText.contains(target);
7550      
7551      
7552      if (isToggleClick || isFolderTextClick) {
7553        const folderId = parseInt(listItem.dataset.id, 10);
7554        
7555        if (folderId && !isNaN(folderId)) {
7556          // Prevent SortableJS from intercepting this click
7557          e.stopPropagation();
7558          e.stopImmediatePropagation();
7559          toggleFolder(folderId, e);
7560        }
7561      }
7562    }, true); // Use capture phase to run before SortableJS
7563    
7564    galleryTree.dataset.folderToggleHandlerAdded = 'true';
7565  }
7566}
7567
7568// NEW HELPER FUNCTION: Gets the nesting level of an element
7569function getElementNestingLevel(element) {
7570  let level = 0;
7571  let current = element;
7572  
7573  // Count how many nested-sortable parents this element has
7574  while (current && current.parentElement) {
7575    if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) {
7576      level++;
7577    }
7578    current = current.parentElement;
7579  }
7580  
7581  return level;
7582}
7583
7584// NEW HELPER FUNCTION: Determines if an item would become nested
7585function wouldBeNestedItem(draggedEl, relatedEl, event) {
7586  if (!relatedEl || !draggedEl) return false;
7587  
7588  // Get mouse position
7589  const mouseX = event.clientX;
7590  
7591  // Get the bounding rect of the related element
7592  const rect = relatedEl.getBoundingClientRect();
7593  
7594  // Determine the nesting threshold - how far from the left edge
7595  // the mouse needs to be to consider it a nesting operation
7596  const nestingThreshold = 25; // pixels from left edge
7597  
7598  // Check if mouse is within the nesting threshold from the left edge
7599  const distanceFromLeft = mouseX - rect.left;
7600  
7601  // Check if we should nest (mouse is within threshold of left edge)
7602  const wouldNest = distanceFromLeft <= nestingThreshold;
7603  
7604  // Log the calculation for debugging
7605  console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`);
7606  
7607  return wouldNest;
7608}
7609
7610/**
7611 * Helper function to find gallery by ID in a tree structure
7612 * @param {Array} galleries - The gallery array to search
7613 * @param {number} id - The ID to find
7614 * @returns {Object|null} - The found gallery or null
7615 */
7616function findGalleryById(galleries, id) {
7617  if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) {
7618    return window.HelpSystem.getDemoRecord(id);
7619  }
7620  if (!Array.isArray(galleries)) return null;
7621  
7622  // First check at the current level
7623  const directMatch = galleries.find(g => g.id == id);
7624  if (directMatch) return directMatch;
7625  
7626  // Then check in children
7627  for (const gallery of galleries) {
7628    if (gallery.children && gallery.children.length > 0) {
7629      const childMatch = findGalleryById(gallery.children, id);
7630      if (childMatch) return childMatch;
7631    }
7632  }
7633  
7634  return null;
7635}
7636
7637/** Detect page menu items even when isPage flag was not persisted */
7638function isPageMenuItem(item) {
7639  if (!item) return false;
7640  if (item.isPage === true) return true;
7641  if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true;
7642  if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true;
7643  return false;
7644}
7645
7646/**
7647 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior.
7648 * Never infer this from children or parentId; legacy items must retain their existing behavior.
7649 */
7650function isNavigableMenuParent(item) {
7651  if (!item || item.actsAsMenuParent !== true) return false;
7652  if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) {
7653    return false;
7654  }
7655  return isPageMenuItem(item) || item.isIntegrated === true;
7656}
7657
7658/**
7659 * Get all gallery IDs from a tree structure
7660 * @param {Array} galleries - Array of galleries
7661 * @returns {Array} - Flat array of all IDs
7662 */
7663function getAllGalleryIds(galleries) {
7664  if (!Array.isArray(galleries)) return [];
7665  
7666  let ids = [];
7667  
7668  galleries.forEach(gallery => {
7669    if (gallery && gallery.id) {
7670      ids.push(gallery.id);
7671    }
7672    
7673    // Add child IDs if any
7674    if (gallery.children && Array.isArray(gallery.children)) {
7675      ids = ids.concat(getAllGalleryIds(gallery.children));
7676    }
7677  });
7678  
7679  return ids;
7680}
7681
7682/**
7683 * Enhanced count function that handles edge cases
7684 * @param {Array} galleryArray - The array to count
7685 * @returns {number} - The total count of galleries
7686 */
7687function countGalleries(galleryArray) {
7688  if (!Array.isArray(galleryArray)) return 0;
7689
7690  // Menu items are stored flat and also get a derived children[] at render time.
7691  // Counting children recursively double-counts those items and falsely triggers
7692  // restoreFromBackup after a successful drop.
7693  const ids = new Set();
7694  function walk(items) {
7695    if (!Array.isArray(items)) return;
7696    items.forEach(function(gallery) {
7697      if (!gallery) return;
7698      if (gallery.id != null) ids.add(gallery.id);
7699      if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children);
7700    });
7701  }
7702  walk(galleryArray);
7703  return ids.size;
7704}
7705
7706/**
7707 * Enhanced backup restoration function with better debugging and notification
7708 */
7709function restoreFromBackup() {
7710  console.warn('Restoring galleries from backup due to invalid operation');
7711  
7712  // Verify we have a valid backup
7713  if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) {
7714    console.error('No valid backup available for restoration');
7715    
7716    // Create a backup from the current DOM as a last resort
7717    try {
7718      console.log('Attempting to rebuild from DOM structure...');
7719      rebuildGalleriesFromDOM();
7720      return;
7721    } catch (rebuildError) {
7722      console.error('Failed to rebuild from DOM:', rebuildError);
7723      // Continue with normal user notification
7724    }
7725  } else {
7726    // Restore from backup
7727    galleries = JSON.parse(JSON.stringify(galleriesBackup));
7728  }
7729  
7730  // Re-render the tree with the restored data
7731  renderGalleries();
7732  
7733  // Force reinitialize nested sortables after restoration
7734  setTimeout(() => {
7735    initializeNestedSortables();
7736  }, 300);
7737}
7738
7739/**
7740 * Emergency recovery function that tries to rebuild galleries from DOM
7741 * This is a last resort when backup restoration fails
7742 */
7743function rebuildGalleriesFromDOM() {
7744  console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure');
7745  
7746  // Create a new galleries array
7747  const rebuiltGalleries = [];
7748  
7749  // This will track items we've already processed
7750  const processedIds = new Set();
7751  
7752  // Function to process a container and its children
7753  function processContainer(container, parentId = null) {
7754    // Get all immediate list items
7755    const items = container.querySelectorAll(':scope > li');
7756    
7757    items.forEach(item => {
7758      // Get item ID from data attribute
7759      const id = parseInt(item.dataset.id);
7760      if (isNaN(id)) return; // Skip if no valid ID
7761      
7762      // Skip already processed items to avoid duplicates
7763      if (processedIds.has(id)) return;
7764      processedIds.add(id);
7765      
7766      // Try to find the original gallery data
7767      let galleryData = null;
7768      
7769      // First check current galleries array
7770      const existingGallery = findGalleryById(galleries, id);
7771      if (existingGallery) {
7772        galleryData = { ...existingGallery };
7773        delete galleryData.children; // We'll rebuild the hierarchy
7774      }
7775      // If not found, check if we have a backup
7776      else if (galleriesBackup && Array.isArray(galleriesBackup)) {
7777        const backupGallery = findGalleryById(galleriesBackup, id);
7778        if (backupGallery) {
7779          galleryData = { ...backupGallery };
7780          delete galleryData.children; // We'll rebuild the hierarchy
7781        }
7782      }
7783      
7784      // If we still don't have data, create minimal placeholder
7785      if (!galleryData) {
7786        // Try to extract title from DOM
7787        const titleElement = item.querySelector('.menu-item');
7788        const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`;
7789        
7790        galleryData = {
7791          id: id,
7792          title: title,
7793          visible: !item.classList.contains('hidden-gallery')
7794        };
7795      }
7796      
7797      // Update parent ID to match DOM structure
7798      galleryData.parentId = parentId;
7799      
7800      // Add to rebuilt galleries
7801      rebuiltGalleries.push(galleryData);
7802      
7803      // Process children if any
7804      const childContainer = item.querySelector('ol.nested-sortable');
7805      if (childContainer) {
7806        processContainer(childContainer, id);
7807      }
7808    });
7809  }
7810  
7811  // Start with root container
7812  const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7813  if (rootContainer) {
7814    processContainer(rootContainer);
7815    
7816    // Replace global galleries array with our rebuilt one
7817    galleries = rebuiltGalleries;
7818    
7819    // Re-render with the rebuilt data
7820    renderGalleries();
7821    
7822    console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`);
7823    return true;
7824  } else {
7825    console.error('RECOVERY FAILED: Root container not found');
7826    return false;
7827  }
7828}
7829
7830// Make sure these functions are available to the global scope
7831window.initializeNestedSortables = initializeNestedSortables;
7832window.restoreFromBackup = restoreFromBackup;
7833window.getElementNestingLevel = getElementNestingLevel;
7834window.wouldBeNestedItem = wouldBeNestedItem;
7835
7836
7837
7838function hasDescendant(itemId, possibleDescendantId) {
7839// Check if possibleDescendantId is a descendant of itemId
7840const children = galleries.filter(g => g.parentId === itemId);
7841
7842if (children.some(child => child.id === possibleDescendantId)) {
7843return true;
7844}
7845
7846return children.some(child => hasDescendant(child.id, possibleDescendantId));
7847}
7848
7849function updateGalleryStructure() {
7850// Create a new array to hold the updated gallery structure
7851const newGalleries = [];
7852
7853// Function to recursively process nested lists
7854function processNestedList(container, parentId = null) {
7855if (!container || !container.children) {
7856  console.warn('Invalid container in processNestedList', container);
7857  return;
7858}
7859
7860const items = container.children;
7861
7862Array.from(items).forEach(item => {
7863  if (item.tagName !== 'LI') return; // Skip non-list items
7864  
7865  const id = parseInt(item.dataset.id);
7866  if (isNaN(id)) {
7867    console.warn('Invalid ID in list item:', item);
7868    return;
7869  }
7870  
7871  const gallery = galleries.find(g => g.id === id);
7872  
7873  if (gallery) {
7874    // Create a new gallery object without children
7875    const newGallery = { ...gallery };
7876    if (newGallery.children) delete newGallery.children;
7877    
7878    // Update parent ID
7879    newGallery.parentId = parentId;
7880    
7881    // Add to new galleries array
7882    newGalleries.push(newGallery);
7883    
7884    // Process any nested OL with class 'nested-sortable' within this item
7885    const nestedList = item.querySelector('ol.nested-sortable');
7886    if (nestedList) {
7887      processNestedList(nestedList, id);
7888    }
7889  } else {
7890    console.warn(`Gallery with ID ${id} not found in galleries array`);
7891  }
7892});
7893}
7894
7895// Start processing from the root list
7896const rootList = document.querySelector('#galleryTree > ol.nested-sortable');
7897if (rootList) {
7898try {
7899  processNestedList(rootList);
7900  
7901  // Verify that we haven't lost any galleries
7902  if (newGalleries.length < galleries.length) {
7903    console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`);
7904    
7905    // Find missing galleries
7906    const originalIds = galleries.map(g => g.id);
7907    const newIds = newGalleries.map(g => g.id);
7908    const missingIds = originalIds.filter(id => !newIds.includes(id));
7909    
7910    if (missingIds.length > 0) {
7911      console.error('Missing gallery IDs:', missingIds);
7912      
7913      // Add missing galleries to the new structure
7914      missingIds.forEach(id => {
7915        const missingGallery = galleries.find(g => g.id === id);
7916        if (missingGallery) {
7917          // Add it as a top-level item
7918          const newGallery = { ...missingGallery, parentId: null };
7919          if (newGallery.children) delete newGallery.children;
7920          newGalleries.push(newGallery);
7921          console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`);
7922        }
7923      });
7924    }
7925  }
7926  
7927  // Update the galleries array with the new structure
7928  galleries = newGalleries;
7929  if (typeof window !== 'undefined') {
7930    window.galleries = galleries;
7931  }
7932  
7933  // Save the updated structure
7934  saveGalleries();
7935} catch (error) {
7936  console.error('Error in updateGalleryStructure:', error);
7937  throw error; // Re-throw to trigger the backup restore
7938}
7939} else {
7940console.warn('Root list not found');
7941throw new Error('Root list not found'); // Throw error to trigger backup restore
7942}
7943}
7944
7945function toggleSubmenu(id, event) {
7946// Ensure the event doesn't interfere with drag operations
7947event.stopPropagation();
7948
7949const listItem = getGalleryTreeListItem(id, event);
7950if (listItem) {
7951// Toggle the expanded class
7952listItem.classList.toggle('expanded');
7953
7954// Find the submenu toggle icon and rotate it
7955const toggleIcon = listItem.querySelector('.toggle-icon');
7956if (toggleIcon) {
7957  toggleIcon.classList.toggle('rotated');
7958}
7959}
7960}
7961
7962/**
7963 * Improved slugify function with robust character handling
7964 * @param {string} text - The text to convert to a slug
7965 * @returns {string} A URL-friendly slug
7966 */
7967function slugify(text) {
7968  // Guard against null or empty input
7969  if (!text || text.trim() === '') {
7970    return 'page-' + Date.now();
7971  }
7972  
7973  // First convert to lowercase
7974  let slug = text.toLowerCase();
7975  
7976  // Replace spaces with hyphens
7977  slug = slug.split(' ').join('-');
7978  
7979  // Filter out unwanted characters (keep only a-z, 0-9, and hyphens)
7980  let filtered = '';
7981  for (let i = 0; i < slug.length; i++) {
7982    const char = slug.charAt(i);
7983    if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') {
7984      filtered += char;
7985    }
7986  }
7987  slug = filtered;
7988  
7989  /*
7990  // Clean up multiple hyphens
7991  while (slug.indexOf('--') !== -1) {
7992    slug = slug.replace('--', '-');
7993  }
7994    */
7995  
7996  // Remove leading and trailing hyphens
7997  if (slug.startsWith('-')) {
7998    slug = slug.substring(1);
7999  }
8000  if (slug.endsWith('-')) {
8001    slug = slug.substring(0, slug.length - 1);
8002  }
8003  
8004  // If slug is empty after processing, use a default
8005  if (!slug || slug === '-') {
8006    return 'page-' + Date.now();
8007  }
8008  
8009  return slug;
8010}
8011
8012// Update the addPage function to use our new robust slugify function
8013/**
8014 * Adds a new page to the galleries array
8015 * @returns {number|null} The new page ID or null if error
8016 */
8017function addPage() {
8018  try {
8019    // Get the page title
8020    const titleInput = document.getElementById('galleryTitle');
8021    if (!titleInput) {
8022      console.error('Page title input not found');
8023      return;
8024    }
8025    const title = titleInput.value.trim();
8026
8027    // Validate title is not empty
8028    if (!title) {
8029      alert('Please enter a page title');
8030      return;
8031    }
8032
8033    // Get parent ID with error handling
8034    const parentSelect = document.getElementById('galleryParent');
8035    const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null;
8036
8037    // Generate a unique ID for the page
8038    const id = Date.now();
8039    const pageId = `page_${id}`;
8040
8041    // Calculate slug using our more robust slugify function
8042    const pageSlug = slugify(title);
8043 
8044    // Create the page object with explicit properties
8045    const page = {
8046      id: id,
8047      title: title,
8048      isPage: true,
8049      isIntegrated: true,
8050      isSubmenu: false,
8051      pageId: pageId,
8052      visible: true,
8053      parentId: parentId,
8054      siteId: siteId,
8055      // Set slug and URL explicitly
8056      slug: pageSlug,
8057      url: '/' + pageSlug
8058    };
8059
8060    console.log(`Creating new page with ID: ${id}, title: "${title}"`);
8061
8062    // Add to galleries array
8063    galleries.push(page);
8064    
8065    // CRITICAL: Ensure window.galleries is synchronized
8066    if (typeof window.galleries === 'undefined') {
8067      window.galleries = galleries;
8068    } else {
8069      // Make sure the page is in window.galleries too
8070      const existingIndex = window.galleries.findIndex(g => g.id === id);
8071      if (existingIndex >= 0) {
8072        window.galleries[existingIndex] = page;
8073      } else {
8074        window.galleries.push(page);
8075      }
8076    }
8077
8078    // Initialize empty page elements array in PageManager
8079    if (window.PageManager) {
8080      window.PageManager.elements[pageId] = [];
8081    }
8082
8083    // Update UI first
8084    renderGalleries();
8085    clearAddForm();
8086    toggleAddForm();
8087
8088    // Set as active page
8089    console.log(`Setting new page "${page.title}" as active`);
8090    activeGalleryId = id;
8091    window.activeGalleryId = id;
8092    
8093    // Update active states in the menu
8094    updateActiveStates();
8095
8096    // Save to server
8097    saveGalleries().then(() => {
8098      console.log(`Page "${page.title}" saved successfully`);
8099      
8100      // Double check that our gallery is still in the array
8101      const pageExists = galleries.some(g => g.id === id);
8102      console.log(`Page exists in galleries array: ${pageExists}`);
8103      
8104      if (pageExists) {
8105        // Load the new page after a slight delay to ensure DOM is updated
8106        setTimeout(() => {
8107          console.log(`Loading new page "${page.title}" with ID: ${id}`);
8108          loadPage(id);
8109        }, 50);
8110      } else {
8111        console.error(`Page with ID ${id} not found in galleries array after save`);
8112      }
8113    }).catch(error => {
8114      alert('Error saving page. Please try again.');
8115    });
8116
8117    return id;
8118  } catch (error) {
8119    console.error('Error adding page:', error);
8120    alert('Error adding page. Please try again.');
8121    return null;
8122  }
8123}
8124
8125/**
8126 * Ensures a slug is unique among existing galleries
8127 * @param {string} slug - The base slug to check
8128 * @param {Array} existingGalleries - Array of existing gallery configs
8129 * @returns {string} A unique slug
8130 */
8131function ensureUniqueSlug(slug, existingGalleries) {
8132  // Guard against invalid slugs
8133  if (!slug || slug === '-') {
8134    slug = 'page-' + Date.now();
8135  }
8136
8137  let finalSlug = slug;
8138  let counter = 1;
8139  
8140  // Check if slug already exists
8141  while (existingGalleries.some(g => g.slug === finalSlug)) {
8142    finalSlug = `${slug}-${counter}`;
8143    counter++;
8144  }
8145  
8146  return finalSlug;
8147}
8148
8149/**
8150 * Adds a new gallery based on the selected type
8151 * @returns {number|null} The new gallery ID or null if error
8152 */
8153function addGallery() {
8154  try {
8155    // Get the gallery title
8156    const titleInput = document.getElementById('galleryTitle');
8157    if (!titleInput) {
8158      console.error('Gallery title input not found');
8159      return;
8160    }
8161    const title = titleInput.value.trim();
8162    
8163    // Get the selected gallery type
8164    const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked');
8165    if (!galleryTypeRadio) {
8166      console.error('No gallery type selected');
8167      alert('Please select a gallery type');
8168      return;
8169    }
8170
8171    const galleryType = galleryTypeRadio.value;
8172    console.log("Selected gallery type:", galleryType);
8173
8174    // If this is a page, use the addPage function
8175    if (galleryType === 'page') {
8176      console.log("Creating page instead of gallery");
8177      return addPage();
8178    }
8179
8180    // For spacer type, we don't require a title
8181    if (galleryType !== 'spacer' && !title) {
8182      alert('Please enter a gallery title');
8183      return;
8184    }
8185
8186    // Continue with gallery creation logic
8187    // Generate unique ID
8188    const id = Date.now();
8189    
8190    // Get parent gallery if selected
8191    const parentSelect = document.getElementById('galleryParent');
8192    const parentId = parentSelect.value ? parseInt(parentSelect.value) : null;
8193    
8194    // Get URL if this is an external gallery
8195    const galleryUrl = document.getElementById('galleryUrl');
8196    const url = galleryTypeRadio.value === 'external' && galleryUrl 
8197      ? galleryUrl.value.trim() 
8198      : "";
8199    
8200    // Set basic properties
8201    const gallery = {
8202      id: id,
8203      title: title || "Spacer", // Use "Spacer" as internal title if no title provided
8204      parentId: parentId,
8205      visible: true
8206    };
8207    
8208    // Set type-specific properties
8209    if (galleryTypeRadio.value === 'spacer') {
8210      // Spacer properties
8211      gallery.isSpacer = true;
8212      gallery.title = title || "Spacer"; // For internal reference
8213    } else if (galleryTypeRadio.value === 'integrated') {
8214      // Generate pageId for integrated galleries
8215      gallery.isIntegrated = true;
8216      gallery.pageId = generatePageId();
8217      
8218      // Generate and ensure unique slug
8219      const baseSlug = slugify(title);
8220      gallery.slug = ensureUniqueSlug(baseSlug, galleries);
8221      gallery.url = "/" + gallery.slug;
8222      
8223      // Get the siteId from multiple possible sources
8224      const siteId = window.Parameters?.siteId || 
8225                    window.siteId || 
8226                    document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') ||
8227                    '';
8228      
8229      // Add galleryOptions with GUID=siteId as the manualCollectionName
8230      gallery.galleryOptions = {
8231        manualCollectionName: `GUID=${siteId}`,
8232        isIntegratedGallery: true,
8233        pageId: gallery.pageId,  // Store pageId for reference
8234        siteId: siteId
8235      };
8236    } else if (galleryTypeRadio.value === 'folder') {
8237      gallery.isFolder = true;
8238      gallery.isCollapsed = true; // Make folders closed by default
8239    } else if (galleryTypeRadio.value === 'external') {
8240      // External URL
8241      gallery.url = url;
8242      gallery.isExternal = true; // Add this flag to identify external URLs
8243    }
8244    
8245    console.log(`Creating new gallery with ID: ${id}, title: "${title}"`);
8246    
8247    // Add to galleries array
8248    galleries.push(gallery);
8249    
8250    // CRITICAL: Ensure window.galleries is synchronized
8251    if (typeof window.galleries === 'undefined') {
8252      window.galleries = galleries;
8253    } else {
8254      // Make sure the gallery is in window.galleries too
8255      const existingIndex = window.galleries.findIndex(g => g.id === id);
8256      if (existingIndex >= 0) {
8257        window.galleries[existingIndex] = gallery;
8258      } else {
8259        window.galleries.push(gallery);
8260      }
8261    }
8262    
8263    // Update UI first
8264    renderGalleries();
8265    clearAddForm();
8266    toggleAddForm();
8267    
8268    // Save galleries after UI update
8269    saveGalleries().then(() => {
8270      console.log(`Gallery "${gallery.title}" saved successfully`);
8271      
8272      // Double check that our gallery is still in the array
8273      const galleryExists = galleries.some(g => g.id === id);
8274      console.log(`Gallery exists in galleries array: ${galleryExists}`);
8275      
8276      // For non-folder types, load the new gallery content after a slight delay
8277      if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) {
8278        setTimeout(() => {
8279          console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`);
8280          if (gallery.isIntegrated) {
8281            loadGallery(id);
8282          } else if (gallery.isExternal && gallery.url) {
8283            // For external URLs, set the iframe src and mark active after navigation target is ready
8284            activeGalleryId = id;
8285            window.activeGalleryId = id;
8286            updateActiveStates();
8287            const frame = document.getElementById('galleryFrame');
8288            if (frame) frame.src = gallery.url;
8289          }
8290        }, 50);
8291      }
8292    }).catch(error => {
8293      console.error(`Error saving gallery "${gallery.title}":`, error);
8294      alert('Error saving gallery. Please try again.');
8295    });
8296    
8297    // Return the new gallery ID
8298    return id;
8299  } catch (error) {
8300    console.error('Error adding gallery:', error);
8301    alert('Error adding gallery. Please try again.');
8302  }
8303}
8304
8305function debugDOMState(message) {
8306console.log(`[DEBUG] ${message}`);
8307const galleryContainer = document.querySelector('.gallery-container');
8308const iframe = document.getElementById('galleryFrame');
8309const pageContainers = document.querySelectorAll('.page-container');
8310
8311console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`);
8312console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`);
8313console.log(`- Page containers: ${pageContainers.length} found`);
8314
8315pageContainers.forEach((container, index) => {
8316console.log(`  - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`);
8317});
8318}
8319
8320
8321/**
8322* Global helper: whether edit tools are explicitly toggled open.
8323* Do not use Parameters.isInEditor — that is also true for logged-in admins (cache bypass).
8324* Do not infer from sidebar/page .editing alone (can desync from the toggle).
8325*/
8326window.isInEditMode = function() {
8327  if (document.body.classList.contains('edit-mode-active')) {
8328    return true;
8329  }
8330  if (typeof window.isEditing === 'boolean') {
8331    return window.isEditing;
8332  }
8333  return false;
8334};
8335
8336/**
8337* This function synchronizes edit mode state across all components
8338*/
8339window.updateGlobalEditState = function(isEditing) {
8340console.log('Updating global edit state:', isEditing);
8341
8342// Update global variable
8343window.isEditing = isEditing;
8344
8345// Update Parameters object if it exists
8346if (window.Parameters) {
8347window.Parameters.isInEditor = isEditing;
8348}
8349
8350// Update sidebar class
8351const sidebar = document.querySelector('.sidebar');
8352if (sidebar) {
8353if (isEditing) {
8354  sidebar.classList.add('editing');
8355} else {
8356  sidebar.classList.remove('editing');
8357}
8358}
8359
8360// Update page containers
8361const pageContainers = document.querySelectorAll('.page-container');
8362pageContainers.forEach(container => {
8363if (isEditing) {
8364  container.classList.add('editing');
8365} else {
8366  container.classList.remove('editing');
8367}
8368});
8369
8370// Notify all components via event
8371document.dispatchEvent(new CustomEvent('edit-mode-changed', {
8372detail: { editing: isEditing }
8373}));
8374
8375if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') {
8376  window.syncContentMarginToHorizontalHeader();
8377}
8378
8379if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8380  window.syncGalleryOptionsButtonVisibility();
8381}
8382
8383// Gallery Options panel styling depends on body.edit-mode-active — hide it when leaving edit mode
8384if (!isEditing && typeof window.closeOptionsPanel === 'function') {
8385  window.closeOptionsPanel();
8386}
8387};
8388
8389/** Show or hide gallery options (#editorButton) based on edit mode and active content type */
8390window.syncGalleryOptionsButtonVisibility = function() {
8391  const btn = document.getElementById('editorButton');
8392  if (!btn) return;
8393
8394  const inEditMode = document.body.classList.contains('edit-mode-active');
8395  // When content type is explicitly gallery, ignore leftover .page-container nodes
8396  // (common when Help sandbox switches from a page to Example Gallery).
8397  const onPageView = window._activeContentType === 'gallery'
8398    ? false
8399    : (window._activeContentType === 'page' ||
8400      !!document.querySelector('.gallery-container .page-container'));
8401  let isActiveGallery = false;
8402  const activeId = window.activeGalleryId;
8403  const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null);
8404  const activeItem = activeId != null && list && typeof findGalleryById === 'function'
8405    ? findGalleryById(list, activeId)
8406    : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null);
8407  if (activeId != null && inEditMode && !onPageView) {
8408    isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal);
8409  }
8410
8411  const shouldShow = inEditMode && !onPageView && isActiveGallery;
8412  if (shouldShow) {
8413    if (window.Parameters) {
8414      window.Parameters.isInEditor = true;
8415    }
8416    btn.classList.remove('gallery-options-force-hidden');
8417    btn.style.removeProperty('display');
8418    btn.style.setProperty('display', 'flex', 'important');
8419    btn.style.pointerEvents = 'auto';
8420  } else {
8421    btn.classList.add('gallery-options-force-hidden');
8422    btn.style.setProperty('display', 'none', 'important');
8423    btn.style.pointerEvents = '';
8424  }
8425};
8426
8427document.addEventListener('gallery-loaded', function() {
8428  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8429    window.syncGalleryOptionsButtonVisibility();
8430  }
8431});
8432
8433document.addEventListener('page-loaded', function() {
8434  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8435    window.syncGalleryOptionsButtonVisibility();
8436  }
8437});
8438
8439/**
8440 * Wait for PageManager after CDN→proxy late script load (ISP/Firefox clients).
8441 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes.
8442 */
8443function whenPageManagerReady(callback, options) {
8444  const maxAttempts = (options && options.maxAttempts) || 50;
8445  const intervalMs = (options && options.intervalMs) || 100;
8446  let attempts = 0;
8447  const run = function() {
8448    if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8449      callback(window.PageManager);
8450      return;
8451    }
8452    attempts += 1;
8453    if (attempts >= maxAttempts) {
8454      console.error('❌ [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8455      if (options && typeof options.onTimeout === 'function') options.onTimeout();
8456      return;
8457    }
8458    if (attempts === 1 || attempts % 10 === 0) {
8459      console.warn('⏳ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts);
8460    }
8461    setTimeout(run, intervalMs);
8462  };
8463  run();
8464}
8465
8466/**
8467 * loadPage function that safely handles undefined galleries
8468 * @param {number} id - The gallery ID
8469 * @param {Event} event - Optional event object
8470 */
8471function loadPage(id, event){
8472  if (event && event.target && event.target.isConnected === false) {
8473    console.log('loadPage: Ignoring navigation from detached element');
8474    return;
8475  }
8476  if (event) {
8477    event.preventDefault();
8478    event.stopPropagation();
8479    const now = Date.now();
8480    const lastCallTime = window._lastPageLoadTime || 0;
8481    window._lastPageLoadTime = now;
8482    if (now - lastCallTime < 100) {
8483      console.log('Ignoring duplicate loadPage call');
8484      return;
8485    }
8486  }
8487  console.log('loadPage: Loading page with gallery ID:', id);
8488
8489  stopAutoAdvanceTimer();
8490  if (typeof dismissGalleryPasswordOverlay === 'function') {
8491    dismissGalleryPasswordOverlay();
8492  }
8493
8494  let galleriesData = window.galleries || galleries;
8495  const gallery = findGalleryById(galleriesData, id);
8496
8497  if (!gallery) {
8498    console.warn('No gallery found with ID:', id, 'for page load.');
8499    return;
8500  }
8501
8502  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
8503
8504  window.activeGalleryId = id;
8505  activeGalleryId = id;
8506  window._activeContentType = 'page';
8507  if (window.EnhancedGallerySystem) {
8508    window.EnhancedGallerySystem.currentGalleryId = null;
8509  }
8510
8511  if (typeof window.removeGalleryScriptsWithPause === 'function') {
8512    window.removeGalleryScriptsWithPause();
8513  }
8514
8515  const galleryContainer = document.querySelector('.gallery-container');
8516  if (galleryContainer) {
8517    galleryContainer.innerHTML = '';
8518  }
8519
8520  if (!gallery.pageId) gallery.pageId = `page_${id}`;
8521  if (!gallery.isPage) gallery.isPage = true;
8522
8523  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8524    window.syncGalleryOptionsButtonVisibility();
8525  }
8526
8527  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
8528  window._pageIdToGalleryId[gallery.pageId] = id;
8529
8530  if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
8531    if (window.PageManager && window.PageManager.elements) {
8532      window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8533    }
8534  } else if (window.PageManager && window.PageManager.elements &&
8535             window.PageManager.elements[gallery.pageId] &&
8536             window.PageManager.elements[gallery.pageId].length > 0) {
8537    gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
8538  }
8539
8540  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8541    try {
8542      window.PageManager.loadPage(gallery.pageId);
8543      const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
8544      if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
8545        window.PageManager.setEditMode(isCurrentlyEditing);
8546      }
8547    } catch (error) {
8548      console.error('Error loading page with PageManager:', error);
8549    }
8550  } else {
8551    console.error('PageManager not found yet — waiting for CDN proxy script');
8552    whenPageManagerReady(function() {
8553      try {
8554        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
8555          console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId);
8556          return;
8557        }
8558        if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) {
8559          window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8560        }
8561        window.PageManager.loadPage(gallery.pageId);
8562        console.log('loadPage: PageManager became available — page render retried');
8563      } catch (error) {
8564        console.error('Error loading page with PageManager after wait:', error);
8565      }
8566    });
8567  }
8568
8569  // Apply menu visibility
8570  const bodyEl = document.body;
8571  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
8572      bodyEl.classList.add('menu-hidden-on-page');
8573  } else {
8574      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
8575  }
8576  applyMenuOverlayClassForGallery(gallery);
8577
8578  if (typeof updateActiveStates === 'function') updateActiveStates();
8579  if (typeof updateMobileTitle === 'function') updateMobileTitle();
8580  if (typeof closeMobileMenu === 'function') closeMobileMenu();
8581  // Close the gallery options panel when navigating to a different page
8582  if (typeof window.closeOptionsPanel === 'function') {
8583    window.closeOptionsPanel();
8584  }
8585  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
8586  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8587    window.syncGalleryOptionsButtonVisibility();
8588  }
8589  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
8590    window.dispatchEvent(new CustomEvent('page-loaded', {
8591      detail: { pageId: gallery.pageId, galleryId: id }
8592    }));
8593  }
8594}
8595
8596
8597
8598
8599
8600function generatePageId() {
8601const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
8602let result = '';
8603const charactersLength = characters.length;
8604for (let i = 0; i < 8; i++) {
8605  result += characters.charAt(Math.floor(Math.random() * charactersLength));
8606}
8607return result;
8608};
8609
8610
8611function normalizeNameForComparison(name) {
8612if (!name) {
8613console.log("Empty name passed to normalization");
8614return '';
8615}
8616
8617console.log(`Normalizing name: "${name}"`);
8618
8619// Step 1: Convert to lowercase
8620let normalized = name.toLowerCase();
8621console.log(`After lowercase: "${normalized}"`);
8622
8623// Step 2: Replace spaces with hyphens (without regex)
8624normalized = normalized.split(' ').join('-');
8625console.log(`After space replacement: "${normalized}"`);
8626
8627// Step 3: Filter out unwanted characters (without regex)
8628let filtered = '';
8629for (let i = 0; i < normalized.length; i++) {
8630const char = normalized.charAt(i);
8631// Keep only a-z, 0-9, and hyphens
8632if ((char >= 'a' && char <= 'z') || 
8633    (char >= '0' && char <= '9') || 
8634    char === '-') {
8635  filtered += char;
8636}
8637}
8638normalized = filtered;
8639console.log(`After character filtering: "${normalized}"`);
8640
8641// Step 4: Clean up multiple hyphens (without regex)
8642while (normalized.indexOf('--') !== -1) {
8643normalized = normalized.split('--').join('-');
8644}
8645
8646// Step 5: Remove leading and trailing hyphens (without regex)
8647if (normalized.charAt(0) === '-') {
8648normalized = normalized.substring(1);
8649}
8650if (normalized.charAt(normalized.length - 1) === '-') {
8651normalized = normalized.substring(0, normalized.length - 1);
8652}
8653
8654// Check if the result is valid
8655if (!normalized || normalized === '-') {
8656console.log(`WARNING: Normalization produced invalid result: "${normalized}"`);
8657} else {
8658console.log(`Final normalized slug: "${normalized}"`);
8659}
8660
8661return normalized;
8662}
8663
8664function ensureUniqueSlug(slug, existingGalleries) {
8665if (!slug) return 'gallery';
8666
8667let finalSlug = slug;
8668let counter = 1;
8669
8670// Check if slug exists in any gallery
8671const slugExists = function(s) {
8672for (let i = 0; i < existingGalleries.length; i++) {
8673  if (existingGalleries[i].slug === s) {
8674    return true;
8675  }
8676}
8677return false;
8678};
8679
8680// Keep incrementing counter until unique
8681while (slugExists(finalSlug)) {
8682finalSlug = slug + '-' + counter;
8683counter++;
8684}
8685
8686return finalSlug;
8687}
8688
8689/**
8690 * Gathers the current complete site configuration from the client-side state
8691 * and triggers a download of the data as a single JSON file.
8692 * This provides a simple way to back up or export the current site structure.
8693 */
8694function downloadCurrentSiteJSON() {
8695  try {
8696    console.log('Gathering current site data for download...');
8697
8698    // Get the current hostname to use in the filename.
8699    const hostname = window.location.hostname.replace('www.', '');
8700
8701    // --- FIX: Ensure we get the most up-to-date menu styles ---
8702    // The MenuStyleCustomizer.settings object is the most reliable source
8703    // for the current styles being used on the client.
8704    const currentMenuStyles = window.MenuStyleCustomizer?.settings || {};
8705
8706    // Get current galleries
8707    const currentGalleries = window.galleries || galleries || [];
8708
8709    // --- STREAMLINED: Process galleries without including image data ---
8710    const enhancedGalleries = currentGalleries.map(gallery => {
8711      // If this is a Classic collection gallery, add note about manual retrieval
8712      if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) {
8713        const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5);
8714        console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`);
8715        
8716        return {
8717          ...gallery,
8718          _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`,
8719          _classicGuid: classicGuid,
8720          _requiresClassicData: true
8721        };
8722      }
8723      
8724      return gallery;
8725    });
8726
8727    // Assemble the complete site configuration object from global state variables.
8728    // This structure mirrors what the `save-config` API endpoint expects, ensuring
8729    // the exported file is a complete and re-importable replica.
8730    const siteData = {
8731      // The core menu/page structure with enhanced Classic collection data.
8732      galleries: enhancedGalleries,
8733      
8734      // Admin emails are managed server-side for security. We export the list if it's
8735      // available on the client from a previous check, but the server remains the
8736      // source of truth. A re-import will not overwrite the admin list.
8737      adminEmails: window.siteConfig?.adminEmails || [],
8738      
8739      // The unique ID for the site.
8740      siteId: window.siteId || window.Parameters?.siteId || '',
8741      
8742      // Custom styling rules for the menu.
8743      menuStyles: currentMenuStyles,
8744      
8745      // All elements configured to appear in the sidebar.
8746      sidebarElements: window.SidebarManager?.elements || [],
8747      
8748      // Global site metadata for SEO and analytics.
8749      siteMetadata: window.siteMetadata || {},
8750      
8751      // Add metadata about Classic collections that need manual data retrieval
8752      _classicCollections: enhancedGalleries
8753        .filter(g => g._requiresClassicData)
8754        .map(g => ({
8755          title: g.title,
8756          guid: g._classicGuid,
8757          note: g._note
8758        }))
8759    };
8760
8761    console.log('Enhanced site data collected for export:', siteData);
8762
8763    // Convert the JavaScript object to a formatted JSON string.
8764    const jsonString = JSON.stringify(siteData, null, 2);
8765
8766    // Create a Blob, which is a file-like object of immutable, raw data.
8767    const blob = new Blob([jsonString], { type: 'application/json' });
8768
8769    // Create a temporary link element to trigger the browser's download functionality.
8770    const url = URL.createObjectURL(blob);
8771    const a = document.createElement('a');
8772    a.href = url;
8773    a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json'
8774    document.body.appendChild(a);
8775    a.click();
8776
8777    // Clean up by removing the temporary link and revoking the object URL to free up memory.
8778    document.body.removeChild(a);
8779    URL.revokeObjectURL(url);
8780
8781    console.log('Enhanced JSON download initiated successfully.');
8782
8783    // Show a helpful message about Classic collections
8784    const classicCount = siteData._classicCollections?.length || 0;
8785    
8786    if (classicCount > 0) {
8787      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 
8787need to manually fetch the Classic collection JSON files.`);
8788    }
8789
8790  } catch (error) {
8791    console.error('Error creating JSON download:', error);
8792    alert('An error occurred while preparing the download. Please check the console.');
8793  }
8794}
8795
8796// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array)
8797function buildHydraClassicNdjson(classicGuid, images) {
8798  const header = {
8799    title: 'Untitled',
8800    description: '',
8801    version: '1',
8802    isClassic: true,
8803    isClassicCollection: true,
8804    classicGuid: 'GUID=' + String(classicGuid || '')
8805  };
8806
8807  const imageArray = Array.isArray(images) ? images.map(function(img, idx) {
8808    var path = '';
8809    if (img) {
8810      path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || '';
8811    }
8812    // Derive filename base from any provided path/filename and normalize extension to .jpg
8813    var fnameBase = '';
8814    if (typeof path === 'string' && path.length > 0) {
8815      var slash = path.lastIndexOf('/');
8816      var raw = slash >= 0 ? path.substring(slash + 1) : path;
8817      // remove query/hash
8818      var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q);
8819      var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h);
8820      // strip extension if present
8821      var dot = raw.lastIndexOf('.');
8822      fnameBase = dot > 0 ? raw.substring(0, dot) : raw;
8823    } else if (img && typeof img.filename === 'string') {
8824      var rf = img.filename;
8825      var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2);
8826      var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2);
8827      var d2 = rf.lastIndexOf('.');
8828      fnameBase = d2 > 0 ? rf.substring(0, d2) : rf;
8829    }
8830    var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.'))
8831      ? img.metadata.extension : '.jpg';
8832    var filename = fnameBase ? (fnameBase + ext) : '';
8833    // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename}
8834    // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path
8835    var guidRaw = String(classicGuid || '').replace(/^GUID=/, '');
8836    var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path;
8837    var rec = {
8838      caption: (img && (img.caption || img.description)) || '',
8839      description: (img && (img.description || img.caption)) || '',
8840      isPreview: idx === 1,
8841      title: (img && img.title) || '',
8842      'alt-text': (img && (img['alt-text'] || img.altText || '')) || '',
8843      image: cdnImageUrl || '',
8844      link: '',
8845      'link-text': '',
8846      filename: filename,
8847      isClassic: true,
8848      isClassicCollection: true,
8849      classicGuid: 'GUID=' + String(classicGuid || '')
8850    };
8851    return rec;
8852  }) : [];
8853
8854  // Build NDJSON as: header line, imageMetadata array line, then one line per full image record
8855  var body = { imageMetadata: imageArray };
8856  var out = [JSON.stringify(header), JSON.stringify(body)];
8857  for (var k = 0; k < imageArray.length; k++) {
8858    out.push(JSON.stringify(imageArray[k]));
8859  }
8860  return out.join('\n') + '\n';
8861}
8862
8863async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) {
8864  // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB)
8865  const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson';
8866  const flyUrl = 'https://hydra-press-v2.fly.dev/publish';
8867  const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys';
8868  
8869  console.error('Starting dual NDJSON upload for:', fileName);
8870  console.error('Content length:', ndjsonContent.length, 'characters');
8871  
8872  // COMPRESSION: Compress for Irys only (not Tigris)
8873  console.error('🔄 Compressing NDJSON with gzip for Irys...');
8874  const encoder = new TextEncoder();
8875  const ndjsonBytes = encoder.encode(ndjsonContent);
8876  const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip'));
8877  const compressedBlob = await new Response(compressedStream).blob();
8878  const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer());
8879  
8880  const originalSize = ndjsonBytes.length;
8881  const compressedSize = compressedBytes.length;
8882  const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
8883  
8884  console.error('📊 Compression results:', {
8885    original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)',
8886    compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)',
8887    ratio: compressionRatio + '%'
8888  });
8889  
8890  const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload
8891  const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT;
8892  
8893  console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)');
8894  
8895  // STEP 1: Upload to Irys (non-blocking, compressed if <800KB)
8896  let irysResult = null;
8897  if (shouldUploadToIrys) {
8898    try {
8899      console.error('🔄 Starting Irys upload (compressed, non-blocking)...');
8900      
8901      // Convert compressed bytes to base64
8902      const compressedBase64 = btoa(String.fromCharCode(...compressedBytes));
8903      
8904      const irysResp = await fetch(irysUrl, {
8905        method: 'POST',
8906        headers: { 'content-type': 'application/json' },
8907        body: JSON.stringify({ 
8908          compressedData: compressedBase64,
8909          originalSize: originalSize,
8910          compressedSize: compressedSize,
8911          siteId: siteId,
8912          pageId: pageId
8913        })
8914      });
8915      
8916      if (irysResp.ok) {
8917        irysResult = await irysResp.json();
8918        console.error('✅ Irys upload successful:', irysResult.txId);
8919        console.error('   Irys Gateway:', irysResult.urls?.irys);
8920        console.error('   ar.io Gateway:', irysResult.urls?.arweave);
8921        
8922        // Log wallet balance and cost
8923        if (irysResult.walletBalance !== undefined) {
8924          console.error('💰 Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR');
8925        }
8926        if (irysResult.uploadCost !== undefined) {
8927          const costUSD = (irysResult.uploadCost * 20).toFixed(4);
8928          if (irysResult.uploadCost === 0) {
8929            console.error('💸 Upload Cost: FREE (0 AR)');
8930          } else {
8931            console.error('💸 Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)');
8932          }
8933        }
8934      } else {
8935        const errorText = await irysResp.text().catch(() => 'Unknown error');
8936        console.warn('⚠️ Irys upload failed (non-critical):', irysResp.status, errorText);
8937      }
8938    } catch (irysError) {
8939      console.warn('⚠️ Irys upload error (non-critical):', irysError.message);
8940      // Continue - Irys failure doesn't block Fly.io upload
8941    }
8942  } else {
8943    console.error('⏭️ Skipping Irys upload (compressed size exceeds 800KB limit)');
8944  }
8945  
8946  // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed)
8947  // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher)
8948  try {
8949    console.error('🔄 Starting Fly.io upload (uncompressed)...');
8950    const flyHeaders = { 'content-type': 'application/json' };
8951    let authToken = null;
8952    try {
8953      if (window.TokenManager) {
8954        if (typeof window.TokenManager.getHydraAuthToken === 'function') {
8955          try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
8956        }
8957        if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
8958          try {
8959            const tw = window.TokenManager.getBestToken();
8960            authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw;
8961          } catch (e) {}
8962        }
8963      }
8964      if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
8965      if (!authToken) authToken = window.authToken;
8966      if (authToken) {
8967        flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
8968          ? 'Bearer ' + authToken
8969          : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
8970        console.error('🔐 Fly.io auth: token attached');
8971      } else {
8972        console.warn('⚠️ No auth token - Fly.io may reject Hydra site upload (401)');
8973      }
8974    } catch (authErr) {
8975      console.warn('⚠️ Auth token lookup failed:', authErr.message);
8976    }
8977    const flyResp = await fetch(flyUrl, {
8978      method: 'POST',
8979      headers: flyHeaders,
8980      body: JSON.stringify({ ndjsonContent, dataFileName: fileName })
8981    });
8982    
8983    console.log('Server response status:', flyResp.status);
8984    
8985    if (!flyResp.ok) {
8986      const errorText = await flyResp.text().catch(() => 'Unknown error');
8987      console.error('❌ Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText);
8988      if (flyResp.status === 401) {
8989        console.error('💡 Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.');
8990      }
8991      return { success: false, irysResult: null };
8992    }
8993    
8994    const flyData = await flyResp.json().catch(() => null);
8995    if (flyData && flyData.urls) {
8996      console.log('✅ Fly.io upload successful:', flyData.urls);
8997      console.log('   Tigris URL:', flyData.urls.urlTigris);
8998      
8999      // Return comprehensive result
9000      const result = {
9001        success: true,
9002        flyUrls: flyData.urls,
9003        irysResult: irysResult,
9004        uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'],
9005        fileSize: compressedSize, // Irys compressed size
9006        originalFileSize: originalSize // Original uncompressed size
9007      };
9008      
9009      console.log('📦 Upload complete:', {
9010        destinations: result.uploadedTo.join(' + '),
9011        txId: irysResult?.txId || 'N/A',
9012        flyUrl: flyData.urls.urlTigris
9013      });
9014      
9015      return result;
9016    } else {
9017      console.warn('Fly.io upload response missing URLs:', flyData);
9018      return { success: false, irysResult: null };
9019    }
9020  } catch (flyError) {
9021    console.error('❌ Fly.io upload error for', fileName + ':', flyError);
9022    return { success: false, irysResult: null };
9023  }
9024}
9025
9026async function importClassicGalleries() {
9027  // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS
9028  function cleanDescriptionLeadingLines(text) {
9029    if (!text || typeof text !== 'string') return text;
9030
9031    let result = text;
9032
9033    // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT>
9034    result = result.replace(
9035      new RegExp(
9036        '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+',
9037        'i'
9038      ),
9039      ''
9040    );
9041
9042    // Remove leading empty P tags
9043    result = result.replace(
9044      new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'),
9045      ''
9046    );
9047
9048    // Remove leading empty FONT tags
9049    result = result.replace(
9050      new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'),
9051      ''
9052    );
9053
9054    // Remove leading BR tags
9055    result = result.replace(
9056      new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'),
9057      ''
9058    );
9059
9060    // Remove any remaining leading whitespace
9061    return result.replace(new RegExp('^\s+', 'i'), '').trim();
9062  }
9063
9064  // Browser-scoped helper: fix multi-line links (template-safe escaped regex)
9065  function fixMultilineLinks(text) {
9066    if (!text || typeof text !== 'string') return text;
9067    return text.replace(
9068      new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'),
9069      function (match) {
9070        const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i'));
9071        if (first) {
9072          return '<a href="' + first[1] + '" target="' + first[2] + '">
9072Click HERE to Add to Cart</a>';
9073        }
9074        return match;
9075      }
9076    );
9077  }
9078
9079  // Browser-scoped helper: convert plain text link phrase to HTML link
9080  function convertPlainTextLinks(text) {
9081    if (!text || typeof text !== 'string') return text;
9082    return text.replace(
9083      new RegExp('Click HERE to Add to Cart', 'g'),
9084      '<a href="#" target="_blank">Click HERE to Add to Cart</a>'
9085    );
9086  }
9087  // Get input elements, including the new checkbox
9088  const guidInputEl = document.getElementById('classicGuid');
9089  const pastedJsonEl = document.getElementById('pastedJson');
9090  const parentIdInputEl = document.getElementById('importParent');
9091  const createSubmenuCheckboxEl = document.getElementById('createSubmenu');
9092  const submenuTitleInputEl = document.getElementById('submenuTitle');
9093  const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox');
9094
9095  // Get values from inputs
9096  const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : '';
9097  const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : '';
9098  const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false;
9099  
9100  let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null;
9101  let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false;
9102  let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : '';
9103
9104  // If replacing the menu, ignore parent/submenu settings
9105  if (replaceMenu) {
9106      parentId = null;
9107      createSubmenu = false;
9108  }
9109
9110  // Validate submenu title if checkbox is checked
9111  if (createSubmenu && !submenuTitle) {
9112    alert('Please provide a title for the submenu');
9113    return;
9114  }
9115
9116  // Show status display
9117  const statusEl = document.getElementById('importStatus');
9118  const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null;
9119  const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null;
9120  const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null;
9121  const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null;
9122
9123  if (statusEl && progressBar && statusMessage) {
9124    statusEl.style.display = 'block';
9125    statusEl.classList.add('importing');
9126    progressBar.style.width = '10%';
9127    statusMessage.textContent = 'Preparing to import...';
9128  }
9129
9130  try {
9131    let classicData;
9132    let classicGuid; // Will hold the primary GUID for image path lookups
9133
9134    // --- LOGIC TO GET DATA: Prioritize Pasted JSON ---
9135    if (pastedJson) {
9136      if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...';
9137      classicData = JSON.parse(pastedJson);
9138
9139      if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9140        throw new Error('Pasted JSON is invalid: "galleries" array not found.');
9141      }
9142      // Use the first available GUID from the pasted data as the default for images
9143      classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId;
9144
9145    } else if (guidInput) {
9146      // Check if this is the new import_ format
9147      if (guidInput.startsWith('import_')) {
9148        if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...';
9149        const importGuid = guidInput.substring(7); // Remove 'import_' prefix
9150        
9151        const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`;
9152        const response = await fetch(jsonUrl);
9153        if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9154        
9155        classicData = await response.json();
9156        
9157        if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9158          throw new Error('Fetched import JSON is invalid: "galleries" array not found.');
9159        }
9160        
9161        // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata
9162        if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...';
9163        
9164        const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`;
9165        
9166        try {
9167          const originalResponse = await fetch(originalJsonUrl);
9168          if (originalResponse.ok) {
9169            const originalData = await originalResponse.json();
9170            
9171            // Get the default language from site configuration
9172            let defaultLanguageID = 'en'; // fallback
9173            if (originalData.languages && Array.isArray(originalData.languages)) {
9174              const defaultLang = originalData.languages.find(lang => lang.isDefault === true);
9175              if (defaultLang) {
9176                defaultLanguageID = defaultLang.id;
9177              }
9178            }
9179            
9180            // Merge image metadata from original JSON into import JSON galleries
9181            if (originalData.galleries && Array.isArray(originalData.galleries)) {
9182              classicData.galleries = classicData.galleries.map(importGallery => {
9183                // Find matching gallery in original data by name or categoryID
9184                
9185                const originalGallery = originalData.galleries.find(orig => 
9186                  orig.title === importGallery.title || // Primary match by title
9187                  orig.name === importGallery.title ||  // Fallback: orig.name matches import.title
9188                  orig.categoryID === importGallery.categoryID // Fallback: categoryID match
9189                );
9190                
9191                
9192                if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) {
9193                  
9194                  // Process all images from original gallery with metadata extraction
9195                    const processedImages = originalGallery.images.map(originalImage => {
9196                    // Apply EXACT same metadata processing logic as Edition Publisher
9197                    let title = originalImage.title || '';
9198                    let dateline = originalImage.dateline || '';
9199                    let caption = originalImage.caption || '';
9200                    
9201                    // Check languageVersions array for metadata (this is where most data is stored)
9202                    if (originalImage.languageVersions && originalImage.languageVersions.length > 0) {
9203                      // Look for site's default language first, then fall back to first available
9204                      let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID);
9205                      if (!langData) {
9206                        langData = originalImage.languageVersions[0];
9207                      }
9208                      
9209                      // For language-enabled JSON files, prioritize languageVersions data
9210                      // Only fall back to top-level if languageVersions data is empty
9211                      if (!title || title.trim() === '') title = langData.title || '';
9212                      if (!dateline || dateline.trim() === '') dateline = langData.dateline || '';
9213                      if (!caption || caption.trim() === '') caption = langData.caption || '';
9214                    }
9215                    
9216                    // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals)
9217                    let description = caption;
9218                    if (caption && caption.includes('TEXTFORMAT')) {
9219                      try {
9220                        // Use properly escaped HTML characters for template literal context
9221                        description = caption
9222                          .replace(/\<TEXTFORMAT[^\>]*\>/g, '')
9223                          .replace(/\<\/TEXTFORMAT\>/g, '')
9224                          .replace(/\<P[^\>]*\>/g, '\<p\>')
9225                          .replace(/\<\/P\>/g, '\<\/p\>')
9226                          .replace(/\<FONT[^\>]*\>/g, '')
9227                          .replace(/\<\/FONT\>/g, '')
9228                          .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>')
9229                          .replace(/\<\/A\>/g, '\<\/a\>')
9230                          .replace(/\<U\>/g, '\<u\>')
9231                          .replace(/\<\/U\>/g, '\<\/u\>')
9232                          .replace(/\<B\>/g, '\<strong\>')
9233                          .replace(/\<\/B\>/g, '\<\/strong\>')
9234                          .replace(/\<I\>/g, '\<em\>')
9235                          .replace(/\<\/I\>/g, '\<\/em\>')
9236                          .replace(/&quot;/g, '"')
9237                          .replace(/&apos;/g, "'")
9238                          .replace(/\s+/g, ' ')
9239                          .trim();
9240                      } catch (error) {
9241                        console.warn('TEXTFORMAT conversion failed, using original caption:', error);
9242                        description = caption;
9243                      }
9244                    }
9245                    
9246                    // Prepend dateline to description if it exists (convert to uppercase)
9247                    if (dateline) {
9248                      const uppercaseDateline = dateline.toUpperCase();
9249                      description = `${uppercaseDateline} ${description}`.trim();
9250                    }
9251                    
9252                    // Apply description fixes
9253                    description = cleanDescriptionLeadingLines(description);
9254                    description = fixMultilineLinks(description);
9255                    description = convertPlainTextLinks(description);
9256                    
9257                  const imageEntry = {
9258                        // Match EXACT structure from Edition Publisher hydraImageEntry
9259                        image: originalImage.image || originalImage.urls?.original || '',
9260                        caption: description, // Use processed description as caption
9261                        description: description, // ALSO set description field (like Edition Publisher)
9262                        title: title,
9263                        'alt-text': title, // Use title as alt-text like Edition Publisher
9264                        link: originalImage.urls?.original || '',
9265                        'link-text': originalImage.urls?.original || '',
9266                        isPreview: false,
9267                        filename: originalImage.filename || '',
9268                        originalUrl: originalImage.image || originalImage.urls?.original || '',
9269                        dateline: dateline
9270                  };
9271
9272                  return imageEntry;
9273                  });
9274                  
9275                  return {
9276                    ...importGallery, // Keep import gallery structure (order, settings, etc.)
9277                    images: processedImages // Replace with processed original images
9278                  };
9279                }
9280                return importGallery;
9281              });
9282            }
9283          } else {
9284            console.warn('Could not fetch original JSON for metadata, using import data only');
9285          }
9286        } catch (error) {
9287          console.warn('Error fetching original JSON for metadata:', error);
9288        }
9289        
9290        // Extract original GUID for image path lookups
9291        // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID
9292        // GUIDs are typically 13 characters, so split on common separators
9293        let baseGuid = importGuid;
9294        const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i);
9295        if (guidMatch) {
9296          baseGuid = guidMatch[1];
9297        } else {
9298          // Fallback: try to extract first 13-character hex string
9299          const hexMatch = importGuid.match(/^([a-f0-9]{13})/i);
9300          if (hexMatch) {
9301            baseGuid = hexMatch[1];
9302          }
9303        }
9304        
9305        // Use the base GUID for image path lookups (e.g., /images/filename.jpg)
9306        classicGuid = baseGuid;
9307        console.log('Successfully loaded hybrid import data with import GUID:', importGuid);
9308        console.log('Using base GUID for image paths:', classicGuid);
9309        
9310             } else {
9311         // Standard GUID format - existing logic
9312         if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9313         classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput;
9314         console.log('Detected standard GUID format, GUID:', classicGuid);
9315         
9316         const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`;
9317         console.log('Fetching classic JSON from:', jsonUrl);
9318         const response = await fetch(jsonUrl);
9319
9320         if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9321         classicData = await response.json();
9322         
9323         if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9324           throw new Error('Fetched JSON is invalid: "galleries" array not found.');
9325         }
9326         
9327         console.log('Successfully loaded classic data with GUID:', classicGuid);
9328       }
9329
9330    } else {
9331      alert('Please either paste site JSON or enter a Classic GUID to import.');
9332      if (statusEl) statusEl.style.display = 'none';
9333      return;
9334    }
9335    
9336    const galleriesForImport = classicData.galleries;
9337    if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`;
9338
9339    // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries
9340    const isImportFormat = !!(guidInput && guidInput.startsWith('import_'));
9341
9342    // --- HIERARCHY RECONSTRUCTION LOGIC ---
9343    
9344    const currentSiteId = window.Parameters?.siteId || siteId;
9345    let submenuParentId = parentId;
9346    const tempNewItems = [];
9347    const originalIdToNewIdMap = {};
9348
9349    // Create the optional top-level submenu if requested
9350    if (createSubmenu) {
9351        const submenuItem = {
9352            id: Date.now(),
9353            title: submenuTitle,
9354            isSubmenu: true,
9355            isCollapsed: true,
9356            visible: true,
9357            parentId: parentId,
9358            siteId: currentSiteId,
9359            children: []
9360        };
9361        galleries.push(submenuItem);
9362        submenuParentId = submenuItem.id;
9363    }
9364
9365    // --- Pass 1: Create new items with correct data and map old IDs to new IDs ---
9366    for (let index = 0; index < galleriesForImport.length; index++) {
9367        const classicItem = galleriesForImport[index];
9368        const newItemId = Date.now() + index + 100;
9369        originalIdToNewIdMap[classicItem.id] = newItemId;
9370
9371        if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`;
9372        if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`;
9373
9374        const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`;
9375        
9376 let slug;
9377        // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean.
9378        // Otherwise (for GUID import), trust the existing slug if it's there.
9379        if (pastedJson) {
9380            slug = normalizeNameForComparison(title);
9381        } else if (classicItem.slug && classicItem.slug.trim() !== '') {
9382            slug = classicItem.slug;
9383        } else {
9384            slug = normalizeNameForComparison(title);
9385        }
9386const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness
9387
9388        // Determine the GUID to use for this specific item's image paths
9389        const itemGuid = classicItem.classicGuid || classicGuid;
9390
9391     let newItem;
9392
9393        // --- Check if it's an External URL ---
9394        if (classicItem.isExternal === true && classicItem.url) {
9395            console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`);
9396            newItem = {
9397                id: newItemId, 
9398                title, 
9399                url: classicItem.url, 
9400                isExternal: true, 
9401                visible: classicItem.visible !== false, 
9402                parentId: null,
9403                siteId: currentSiteId, 
9404                slug: finalSlug,
9405                importSource: 'external-url'
9406            };
9407        }
9408        // --- Check if it's a Page and build its elements ---
9409        else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) {
9410            const pageUniqueId = `page_${newItemId}`;
9411            
9412            let pageElements = [];
9413            
9414            // =================================================================
9415            // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format
9416            // =================================================================
9417            if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) {
9418                console.log(`Using existing pageElements for page: "${title}"`);
9419                
9420                // Check if we have both pageElements and images - create two-column layout
9421                if (classicItem.images && classicItem.images.length > 0) {
9422                    console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`);
9423                    
9424                    // Create metadata element
9425                    pageElements.push({
9426                        id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9427                        metaTitle: classicItem.meta?.title || title,
9428                        metaDescription: classicItem.meta?.description || "",
9429                        metaKeywords: classicItem.meta?.keywords || ""
9430                    });
9431                    
9432                    // Create two-column container
9433                    const leftColumnElements = [];
9434                    const rightColumnElements = [];
9435                    
9436                    // Add images to left column
9437                    const slides = classicItem.images.map((img) => {
9438                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9439                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9440                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9441                        
9442                        return { 
9443                            imageUrl, 
9444                            imageKey, 
9445                            imageFilename, 
9446                            imageType: img.metadata?.mimetype || 'image/jpeg',
9447                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9448                            title: img.title || '',
9449                            byline: img.byline || '',
9450                            dateline: img.dateline || '',
9451                            altText: img.metadata?.alt_text || ''
9452                        };
9453                    });
9454                    
9455                    leftColumnElements.push({
9456                        id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0,
9457                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9458                    });
9459                    
9460                    // Add text content to right column
9461                    classicItem.pageElements.forEach((el, elIndex) => {
9462                        if (el.type === 'text' && el.content) {
9463                            rightColumnElements.push({
9464                                id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex,
9465                                textContent: el.content, textWidth: 70
9466                            });
9467                        }
9468                    });
9469                    
9470                    const columns = [
9471                        { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements },
9472                        { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }
9473                    ];
9474                    
9475                    pageElements.push({
9476                        id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1,
9477                        columns,
9478                        backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9479                        backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9480                    });
9481                    
9482                } else {
9483                    // No images, just transform the pageElements as before
9484                    pageElements = classicItem.pageElements.map((el, elIndex) => {
9485                        const elementId = Date.now() + index + 1000 + elIndex;
9486                        
9487                        // Handle import_guid.json format (type + content)
9488                        if (el.type && el.content) {
9489                            if (el.type === 'text') {
9490                                return {
9491                                    id: elementId,
9492                                    type: 'text',
9493                                    title: 'Text Block',
9494                                    visible: true,
9495                                    position: elIndex,
9496                                    textContent: el.content,
9497                                    textWidth: 100
9498                                };
9499                            } else if (el.type === 'metadata') {
9500                                return {
9501                                    id: elementId,
9502                                    type: 'metadata',
9503                                    title: 'Metadata',
9504                                    visible: true,
9505                                    position: elIndex,
9506                                    metaTitle: el.metaTitle || title,
9507                                    metaDescription: el.metaDescription || '',
9508                                    metaKeywords: el.metaKeywords || ''
9509                                };
9510                            }
9511                        }
9512                        
9513                        // Handle existing format (already has id, position, etc.)
9514                        return {
9515                            ...el,
9516                            id: elementId,
9517                            position: el.position !== undefined ? el.position : elIndex
9518                        };
9519                    });
9520                }
9521
9522            } else {
9523                // Otherwise (for GUID-based import), generate the page structure from scratch.
9524                console.log(`Generating new pageElements for page: "${title}"`);
9525                pageElements.push({
9526                  id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9527                  metaTitle: classicItem.meta?.title || title,
9528                  metaDescription: classicItem.meta?.description || "",
9529                  metaKeywords: classicItem.meta?.keywords || ""
9530                });
9531
9532                // Add images if they exist in the page
9533                if (classicItem.images && classicItem.images.length > 0) {
9534                    const slides = classicItem.images.map((img) => {
9535                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9536                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9537                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9538                        
9539                        return { 
9540                            imageUrl, 
9541                            imageKey, 
9542                            imageFilename, 
9543                            imageType: img.metadata?.mimetype || 'image/jpeg',
9544                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9545                            title: img.title || '',
9546                            byline: img.byline || '',
9547                            dateline: img.dateline || '',
9548                            altText: img.metadata?.alt_text || ''
9549                        };
9550                    });
9551                    
9552                    pageElements.push({
9553                        id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1,
9554                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true
9555                    });
9556                }
9557
9558                pageElements.push({
9559                  id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2,
9560                  textContent: "<p>&nbsp;</p><p><br></p>", textWidth: 100
9561                });
9562
9563                const columns = [];
9564                const leftColumnElements = [];
9565                const rightColumnElements = [];
9566
9567                if (classicItem.images && classicItem.images.length > 0) {
9568                  const slides = classicItem.images.map((img) => {
9569                    const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9570                    const imageKey = `${itemGuid}/images/${imageFilename}`;
9571                    const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9572                    
9573                    // Preserve all image metadata including captions, titles, etc.
9574                    return { 
9575                      imageUrl, 
9576                      imageKey, 
9577                      imageFilename, 
9578                      imageType: img.metadata?.mimetype || 'image/jpeg',
9579                      // Preserve caption information - handle both direct and language version storage patterns
9580                      caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))),
9581                      title: img.title || img.languageVersions?.[0]?.title || '',
9582                      byline: img.byline || img.languageVersions?.[0]?.byline || '',
9583                      dateline: img.dateline || img.languageVersions?.[0]?.dateline || '',
9584                      altText: img.metadata?.alt_text || '',
9585                      // Preserve any additional metadata
9586                      metaTitle: img.metadata?.meta_title || '',
9587                      metaDescription: img.metadata?.meta_description || '',
9588                      metaKeywords: img.metadata?.meta_keywords || '',
9589                      // Preserve gallery-specific data
9590                      sortOrder: img.galleryData?.sortOrder || 0,
9591                      linkURL: img.galleryData?.linkURL || null,
9592                      linkTarget: img.galleryData?.linkTarget || '_self'
9593                    };
9594                  });
9595                  leftColumnElements.push({
9596                    id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0,
9597                    slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9598                  });
9599                }
9600
9601                let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || "";
9602                if (rawDetails && typeof rawDetails === 'string') {
9603                    // =================================================================
9604                    // CHANGE 2: Fix for "Invalid regular expression flags" error.
9605                    // Changed from /.../gi literal to new RegExp('...', 'gi') constructor
9606                    // to prevent syntax errors.
9607                    // =================================================================
9608                    let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), '');
9609                    processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>');
9610                    processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), '');
9611                    processedText = processedText.replace(new RegExp('<B>
9611', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>');
9612                    processedText = processedText.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
9613                    
9614                    // Apply description fixes to processed text
9615                    processedText = cleanDescriptionLeadingLines(processedText);
9616                    processedText = fixMultilineLinks(processedText);
9617                    processedText = convertPlainTextLinks(processedText);
9618                    
9619                    rightColumnElements.push({
9620                      id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0,
9621                      textContent: processedText, textWidth: 70
9622                    });
9623                }
9624                
9625                columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements });
9626                columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements });
9627
9628                pageElements.push({
9629                  id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2,
9630                  columns,
9631                  backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9632                  backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9633                });
9634            }
9635
9636            newItem = {
9637                id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null,
9638                siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements,
9639                classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page'
9640            };
9641                } else if (classicItem.isFolder === true) {
9642            // --- Handle folders from import_guid.json format ---
9643            console.log(`Creating folder: "${title}"`);
9644            newItem = {
9645                id: newItemId, 
9646                title, 
9647                isFolder: true, 
9648                isCollapsed: classicItem.isCollapsed !== false, 
9649                visible: classicItem.visible !== false, 
9650                parentId: null,
9651                siteId: currentSiteId, 
9652                slug: finalSlug,
9653                importSource: 'folder'
9654            };
9655        } else {
9656            // --- This is a Gallery ---
9657            const galleryPageId = generatePageId();
9658            
9659            // --- ENHANCED: Try to fetch and include actual image data with captions ---
9660            let enhancedGalleryOptions = {
9661                manualCollectionName: `GUID=${itemGuid}`, 
9662                categoryId: classicItem.categoryID, 
9663                importedFromClassic: true,
9664                siteId: currentSiteId, 
9665                pageId: galleryPageId
9666            };
9667            
9668            // --- ENHANCED: Preserve gallery display settings from Classic JSON ---
9669            if (classicItem.galleryOptions) {
9670                console.log(`Preserving gallery settings for: ${title}`);
9671                
9672                // Preserve layout and display settings
9673                const preservedSettings = [
9674                    'startInSingles', 'layoutType', 'columns', 'spacing',
9675                    'showDescription', 'showFilename', 'displayAllInfo',
9676                    'navigationMode', 'showTextBlock', 'fixedHeroImage',
9677                    'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration',
9678                    'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor',
9679                    'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity',
9680                    'lightboxCloseColor', 'lightboxArrowColor', 'useTitles',
9681                    'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign',
9682                    'showDescriptionInOverlay', 'includeRolloverImageInOverlay',
9683                    'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles',
9684                    'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames',
9685                    'openMultipleLightboxes', 'batchSize', 'autoplaySingles',
9686                    'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart'
9687                ];
9688                
9689                preservedSettings.forEach(setting => {
9690                    if (classicItem.galleryOptions[setting] !== undefined) {
9691                        enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting];
9692                    }
9693                });
9694                
9695                // Preserve text content
9696                if (classicItem.galleryOptions.horizontalScrollerText) {
9697                    enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText;
9698                }
9699                
9700                // Preserve JSON collections if present
9701                if (classicItem.galleryOptions.jsonCollections) {
9702                    enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections;
9703                } else {
9704                    // Create jsonCollections structure with processed image metadata
9705                    const collectionKey = 'GUID=' + itemGuid;
9706                    const processedImages = classicItem.images || [];
9707                    
9708                    enhancedGalleryOptions.jsonCollections = {
9709                        [collectionKey]: {
9710                            isClassicCollection: true,
9711                            guid: collectionKey,
9712                            metadata: processedImages,
9713                            totalItems: processedImages.length
9714                        }
9715                    };
9716                    
9717                    console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images');
9718
9719                    // Build and try to upload NDJSON for this gallery.
9720                    // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON.
9721                    // Skip NDJSON generation for galleries with no images to avoid creating empty files
9722                    if (processedImages.length > 0) {
9723                      try {
9724                        console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)');
9725                        const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9726                        console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters');
9727                        
9728                        const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9729                      if (uploadResult && uploadResult.success) {
9730                        try {
9731                          // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent)
9732                          if (isImportFormat) {
9733                            const hydraCollectionKey = 'GUID=' + currentSiteId;
9734                            enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9735                            enhancedGalleryOptions.jsonCollections = {
9736                              [hydraCollectionKey]: {
9737                                isClassicCollection: false,
9738                                guid: hydraCollectionKey,
9739                                metadata: {},
9740                                totalItems: processedImages.length
9741                              }
9742                            };
9743                            enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9744                            enhancedGalleryOptions.initialPageUuid = galleryPageId;
9745                            enhancedGalleryOptions.isHydra = true;
9746                            enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9747                            console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)');
9748                          } else {
9749                            enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {};
9750                            enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length;
9751                          }
9752                          
9753                          // Store dual upload metadata if Irys was successful
9754                          if (uploadResult.irysResult && uploadResult.irysResult.txId) {
9755                            enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9756                            enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing
9757                            enhancedGalleryOptions.isPerma = true;
9758                            enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo;
9759                            enhancedGalleryOptions.fileSize = uploadResult.fileSize;
9760                            enhancedGalleryOptions.siteId = currentSiteId;
9761                            enhancedGalleryOptions.pageId = galleryPageId;
9762                            console.log('✅ Dual upload complete - Irys txId:', uploadResult.irysResult.txId);
9763                            console.log('   Uploaded to:', uploadResult.uploadedTo.join(' + '));
9764                          } else {
9765                            console.log('✅ Single upload (Fly.io only) - No Irys data');
9766                          }
9767                        } catch (applyErr) {
9768                          console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr);
9769                        }
9770                      } else {
9771                        console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array');
9772                      }
9773                      } catch (e) {
9774                        console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e);
9775                      }
9776                    } else {
9777                      console.log('Skipping NDJSON generation for "' + title + '" - no images to process');
9778                    }
9779                }
9780                
9781                // Preserve additional metadata
9782                if (classicItem.galleryOptions.siteAlias) {
9783                    enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias;
9784                }
9785                if (classicItem.galleryOptions.initialPageUuid) {
9786                    enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid;
9787                }
9788                if (classicItem.galleryOptions.initialPageAlias) {
9789                    enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias;
9790                }
9791                if (classicItem.galleryOptions.timestamp) {
9792                    enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp;
9793                }
9794            } else if (isImportFormat && (classicItem.images || []).length > 0) {
9795                // import_ format without galleryOptions: still create NDJSON for new Hydra galleries
9796                const processedImages = classicItem.images || [];
9797                const collectionKey = 'GUID=' + itemGuid;
9798                enhancedGalleryOptions.jsonCollections = {
9799                  [collectionKey]: {
9800                    isClassicCollection: true,
9801                    guid: collectionKey,
9802                    metadata: processedImages,
9803                    totalItems: processedImages.length
9804                  }
9805                };
9806                console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)');
9807                try {
9808                  const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9809                  const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9810                  if (uploadResult && uploadResult.success) {
9811                    const hydraCollectionKey = 'GUID=' + currentSiteId;
9812                    enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9813                    enhancedGalleryOptions.jsonCollections = {
9814                      [hydraCollectionKey]: {
9815                        isClassicCollection: false,
9816                        guid: hydraCollectionKey,
9817                        metadata: {},
9818                        totalItems: processedImages.length
9819                      }
9820                    };
9821                    enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9822                    enhancedGalleryOptions.initialPageUuid = galleryPageId;
9823                    enhancedGalleryOptions.isHydra = true;
9824                    enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9825                    if (uploadResult.irysResult?.txId) {
9826                      enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9827                      enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId;
9828                      enhancedGalleryOptions.isPerma = true;
9829                      enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly'];
9830                    }
9831                    console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON');
9832                  }
9833                } catch (e) {
9834                  console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e);
9835                }
9836            }
9837            
9838            // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions
9839            // Image data is only included when importing from Classic GUID directly
9840            console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`);
9841            
9842            newItem = {
9843                id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null,
9844                siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid,
9845                importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug,
9846                galleryOptions: enhancedGalleryOptions
9847            };
9848        }
9849
9850        tempNewItems.push(newItem);
9851    }
9852
9853    // --- Pass 2: Link children to parents using the ID map ---
9854    tempNewItems.forEach(newItem => {
9855        const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id);
9856        const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId));
9857
9858        if (originalItem && originalItem.parentId) {
9859            const newParentId = originalIdToNewIdMap[originalItem.parentId];
9860            if (newParentId) {
9861                newItem.parentId = newParentId;
9862                const parentItem = tempNewItems.find(p => p.id === newParentId);
9863                if (parentItem) {
9864                    parentItem.isSubmenu = true;
9865                    parentItem.isCollapsed = true;
9866                }
9867            } else {
9868                newItem.parentId = submenuParentId;
9869            }
9870        } else {
9871            newItem.parentId = submenuParentId;
9872        }
9873    });
9874
9875    if (replaceMenu) {
9876      console.log("Replacing existing menu structure.");
9877      // If replacing, overwrite the entire galleries array.
9878      galleries = tempNewItems;
9879    } else {
9880      console.log("Appending to existing menu structure.");
9881      // If not replacing, push the new items to the existing array.
9882      galleries.push(...tempNewItems);
9883    }
9884
9885
9886    
9887    if (progressBar) progressBar.style.width = '90%';
9888    if (statusMessage) statusMessage.textContent = 'Saving changes...';
9889
9890    // Pass import flag to skip localStorage for large import operations
9891    await saveGalleries({ isImport: true });
9892
9893    if (progressBar) progressBar.style.width = '100%';
9894    if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`;
9895    if (statusEl) statusEl.classList.remove('importing');
9896
9897    setTimeout(() => {
9898        renderGalleries();
9899        toggleImportClassicForm();
9900        if (progressBar) progressBar.style.width = '0';
9901        if (statusEl) statusEl.style.display = 'none';
9902        if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) {
9903            window.location.reload();
9904        }
9905    }, 1000);
9906
9907  } catch (error) {
9908    console.error('Error importing classic galleries:', error);
9909    if (statusMessage) statusMessage.textContent = `Error: ${error.message}`;
9910    if (progressBar) {
9911      progressBar.style.width = '100%';
9912      progressBar.style.backgroundColor = '#dc3545';
9913    }
9914    if (statusEl) statusEl.classList.remove('importing');
9915  }
9916}
9917
9918
9919
9920/**
9921 * Improved toggleImportClassicForm - Closes other forms first
9922 */
9923function toggleImportClassicForm() {
9924  // First check if user is logged in and in edit mode
9925  if (!isAuthenticated || !isAdmin || !window.isInEditMode()) {
9926    alert('You must be logged in as an admin and in edit mode to import galleries.');
9927    return;
9928  }
9929
9930  const formId = 'importClassicForm';
9931  const form = document.getElementById(formId);
9932  
9933  if (!form) {
9934    console.error('Import form element not found');
9935    return;
9936  }
9937  
9938  // If this form is already open, just close everything
9939  if (window.currentOpenForm === formId) {
9940    closeAllForms();
9941    return;
9942  }
9943  
9944  // Otherwise close all forms and open this one
9945  closeAllForms();
9946  
9947  // Show the form
9948  form.style.display = 'block';
9949  
9950  // Use setTimeout to allow the display change to take effect before adding the class
9951  setTimeout(() => {
9952    form.classList.add('visible');
9953  }, 10);
9954
9955  // Populate parent options
9956  const parentSelect = document.getElementById('importParent');
9957  if (parentSelect) {
9958    parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>';
9959    
9960    // Add all galleries that could be parents (including submenus)
9961    galleries.forEach(gallery => {
9962      // Only include items that are not already marked as imported
9963      if (!gallery.importSource) {
9964        parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
9965      }
9966    });
9967  }
9968
9969  // Set default values
9970  const submenuTitle = document.getElementById('submenuTitle');
9971  if (submenuTitle && !submenuTitle.value) {
9972    submenuTitle.value = 'Classic Galleries';
9973  }
9974
9975  // Reset any previous import status
9976  const statusEl = document.getElementById('importStatus');
9977  if (statusEl) {
9978    statusEl.style.display = 'none';
9979    const progressBar = statusEl.querySelector('.progress-bar');
9980    if (progressBar) {
9981      progressBar.style.width = '0';
9982      progressBar.style.backgroundColor = '#4682B4'; // Reset to blue
9983    }
9984  }
9985  
9986  // Set this as the current open form
9987  window.currentOpenForm = formId;
9988}
9989
9990// Add event listener for the radio buttons to adjust height when page is selected
9991document.addEventListener('DOMContentLoaded', function() {
9992  const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]');
9993  galleryTypeRadios.forEach(radio => {
9994    radio.addEventListener('change', function() {
9995      const addForm = document.getElementById('addForm');
9996      if (addForm && addForm.classList.contains('visible')) {
9997        const isPageSelected = this.value === 'page';
9998        
9999        // Give more height for page type
10000        if (isPageSelected) {
10001          addForm.style.maxHeight = '600px';
10002        } else {
10003          // For other types, measure content height
10004          const contentHeight = addForm.scrollHeight + 30;
10005          addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px';
10006        }
10007      }
10008    });
10009  });
10010});
10011
10012
10013/**
10014 * Improved toggleStyleEditor - Closes other forms first
10015 */
10016function toggleStyleEditor(display = false, event) {
10017  const formId = 'menuStyleEditor';
10018  const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen;
10019
10020  if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) {
10021    if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') {
10022      window.MenuStyleCustomizer._toggleOverlayMinimized(false);
10023    }
10024    return;
10025  }
10026
10027  if (display === false || window.currentOpenForm === formId || overlayOpen) {
10028    closeAllForms();
10029    return;
10030  }
10031
10032  closeAllForms();
10033
10034  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') {
10035    window.MenuStyleCustomizer.toggleOverlay();
10036  }
10037}
10038
10039
10040/**
10041 * This function closes all forms when escape key is pressed
10042 */
10043document.addEventListener('keydown', function(e) {
10044  if (e.key === 'Escape' && window.currentOpenForm) {
10045    if (closeTopmostStyleCustomizerChildOverlay()) {
10046      return;
10047    }
10048    if (window.MenuStyleCustomizer &&
10049        window.MenuStyleCustomizer._overlayOpen &&
10050        window.MenuStyleCustomizer._overlayMinimized) {
10051      return;
10052    }
10053    closeAllForms();
10054  }
10055});
10056
10057/**
10058 * This function closes all forms when clicking outside any form
10059 */
10060document.addEventListener('click', function(e) {
10061  if (!window.currentOpenForm) return;
10062
10063  if (window.MenuStyleCustomizer &&
10064      window.MenuStyleCustomizer._overlayOpen &&
10065      window.MenuStyleCustomizer._overlayMinimized) {
10066    return;
10067  }
10068
10069  // Keep Style Customizer and its nested panels open during in-overlay interactions.
10070  if (e.target.closest(
10071    '.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'
10072  )) {
10073    return;
10074  }
10075
10076  if (e.target.closest('.image-uploader-overlay')) {
10077    return;
10078  }
10079
10080  const currentForm = document.getElementById(window.currentOpenForm);
10081  if (!currentForm) {
10082    if (e.target.closest(
10083      '.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'
10084    )) {
10085      return;
10086    }
10087    closeAllForms();
10088    return;
10089  }
10090
10091  const isInsideForm = currentForm.contains(e.target);
10092  const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null;
10093
10094  if (isInsideForm || isFormToggleButton) {
10095    return;
10096  }
10097
10098  closeAllForms();
10099});
10100
10101// Override the original functions with our improved versions
10102window.toggleAddForm = toggleAddForm;
10103window.toggleStyleEditor = toggleStyleEditor;
10104window.toggleMetadataEditor = toggleMetadataEditor;
10105window.toggleSidebarElementForm = toggleSidebarElementForm;
10106window.toggleImportClassicForm = toggleImportClassicForm;
10107window.closeAllForms = closeAllForms;
10108window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay;
10109
10110
10111/**
10112 * Opens the edit form for a gallery item.
10113 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu)
10114 * @param {number} id - The ID of the gallery item.
10115 * @param {Event} event - The click event.
10116 */
10117function editGallery(id, event) {
10118  event.stopPropagation(); // Prevent event bubbling
10119  const gallery = findGalleryById(galleries, id);
10120  const li = getGalleryTreeListItem(id, event);
10121
10122  if (gallery && li) {
10123    const showUrlField = gallery.isPage || gallery.isIntegrated;
10124    const showExternalUrlField = gallery.isExternal;
10125    const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal &&
10126      (isPageMenuItem(gallery) || gallery.isIntegrated === true);
10127    /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */
10128    const showSidebarSeoFields = !gallery.isPage;
10129    const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10130    const currentExternalUrl = gallery.url || '';
10131
10132    // Clear existing content and rebuild form to ensure event listeners are fresh if needed
10133    li.innerHTML = ''; // Clear previous form if any
10134
10135    const formDiv = document.createElement('div');
10136    formDiv.className = 'edit-form';
10137    formDiv.innerHTML = `
10138        <div class="form-group">
10139           <label>Title:</label>
10140           <input type="text" value="${gallery.title || ''}" class="edit-title">
10141        </div>
10142
10143        ${showUrlField ? `
10144        <div class="form-group">
10145           <label>URL Slug:</label>
10146           <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery">
10147           <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small>
10148        </div>
10149        ` : ''}
10150
10151        ${showExternalUrlField ? `
10152        <div class="form-group">
10153           <label>External URL:</label>
10154           <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com">
10155           <small>Full URL for the external link (e.g., https://example.com/page).</small>
10156        </div>
10157        ` : ''}
10158
10159        ${showSidebarSeoFields ? `
10160        <div class="form-group">
10161           <label>Meta Title (Optional):</label>
10162           <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)">
10163           <small>Overrides site title for this item in search results/browser tabs.</small>
10164        </div>
10165
10166        <div class="form-group">
10167           <label>Meta Description (Optional):</label>
10168           <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea>
10169           <small>Overrides site description for this item in search results.</small>
10170        </div>
10171        ` : `
10172        <div class="form-group">
10173           <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;">
10174             Search title and description for this page are set in <strong>Page → metadata</strong> (the metadata block on the page), not here.
10175           </p>
10176        </div>
10177        `}
10178
10179        <div class="form-group">
10180          <label style="display: flex; align-items: center; gap: 8px;">
10181            <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10182            Hide menu when viewing this page
10183          </label>
10184          <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small>
10185        </div>
10186
10187        <div class="form-group">
10188          <label style="display: flex; align-items: center; gap: 8px;">
10189            <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10190            Show menu as overlay on this page
10191          </label>
10192          <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small>
10193        </div>
10194
10195        ${showMenuParentOption ? `
10196        <div class="form-group">
10197          <label style="display: flex; align-items: center; gap: 8px;">
10198            <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10199            Allow nested menu items
10200          </label>
10201          <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small>
10202        </div>
10203        ` : ''}
10204
10205        <div class="form-group">
10206          <label style="display: flex; align-items: center; gap: 8px;">
10207            <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10208            Password protect this page
10209          </label>
10210          <small>Visitors will need to enter a password to access this page.</small>
10211        </div>
10212
10213        <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};">
10214          <label>Password:</label>
10215          <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password">
10216          <small>Password visitors must enter to access this page.</small>
10217        </div>
10218
10219        <div class="form-group">
10220          <label style="display: flex; align-items: center; gap: 8px;">
10221            <input type="checkbox" class="edit-no-index" ${(() => {
10222              // Explicit setting takes priority;
10222 otherwise default to checked for hidden non-home pages
10223              if (gallery.noIndex === true) return 'checked';
10224              if (gallery.noIndex === false) return '';
10225              return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : '';
10226            })()} style="width: auto; margin-bottom: 0;">
10227            Hide from search engines (noindex)
10228          </label>
10229          <small>When checked, search engines will not index this page. Automatically enabled for hidden pages — uncheck to allow indexing even when hidden.</small>
10230        </div>
10231
10232        <div class="edit-form-actions"> 
10233          <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 
10234          <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 
10235        </div>
10236        <br>
10237    `;
10238    li.appendChild(formDiv); // Append the new form
10239
10240    // Add event listeners programmatically
10241    const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`);
10242    if (saveButton) {
10243        saveButton.addEventListener('click', (e) => saveEdit(id, e));
10244    }
10245
10246    const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`);
10247    if (cancelButton) {
10248        cancelButton.addEventListener('click', () => renderGalleries());
10249    }
10250
10251    // Add password protection toggle functionality
10252    const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected');
10253    const passwordField = formDiv.querySelector('.password-field');
10254    if (passwordProtectedCheckbox && passwordField) {
10255        passwordProtectedCheckbox.addEventListener('change', function() {
10256            passwordField.style.display = this.checked ? 'block' : 'none';
10257        });
10258    }
10259
10260    li.classList.add('edit-mode');
10261  } else {
10262    console.error("Could not find gallery or list item for ID:", id);
10263  }
10264}
10265
10266/**
10267 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use
10268 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first
10269 * and break nested folder toggles in edit mode while horizontal layout is on.
10270 * @param {number} id - Gallery ID
10271 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree)
10272 * @returns {HTMLElement|null}
10273 */
10274function getGalleryTreeListItem(id, event) {
10275  var numericId = typeof id === 'number' ? id : parseInt(String(id), 10);
10276  if (isNaN(numericId)) return null;
10277  var galleryTreeEl = document.getElementById('galleryTree');
10278  if (event && event.target && galleryTreeEl) {
10279    var clicked = event.target.closest('li[data-id]');
10280    if (clicked && galleryTreeEl.contains(clicked)) {
10281      var cid = parseInt(clicked.getAttribute('data-id') || '', 10);
10282      if (cid === numericId) return clicked;
10283    }
10284  }
10285  if (galleryTreeEl) {
10286    var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]');
10287    if (scoped) return scoped;
10288  }
10289  return document.querySelector('li[data-id="' + numericId + '"]');
10290}
10291
10292/**
10293 * Handles click on a folder item to show/hide its contents
10294 * @param {number} id - The gallery ID
10295 * @param {Event} event - The click event
10296 */
10297function toggleFolder(id, event) {
10298  // Ensure the event doesn't interfere with drag operations
10299  if (event) {
10300    event.stopPropagation();
10301  }
10302  
10303  
10304  const gallery = galleries.find(g => g.id === id);
10305  if (!gallery) {
10306    return;
10307  }
10308
10309  if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) {
10310    return;
10311  }
10312  
10313  
10314  // Toggle the collapsed state
10315  gallery.isCollapsed = !gallery.isCollapsed;
10316  
10317  // Find the list item in the DOM (#galleryTree first — not horizontal li.horizontal-submenu-item)
10318  const listItem = getGalleryTreeListItem(id, event);
10319  if (!listItem) {
10320    return;
10321  }
10322  
10323  
10324  // Toggle the expanded class FIRST - this is important for CSS to work correctly
10325  listItem.classList.toggle('expanded', !gallery.isCollapsed);
10326
10327  // Keep ARIA in sync for folder label + chevron (screen readers / keyboard)
10328  const folderExpanded = !gallery.isCollapsed;
10329  listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => {
10330    el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false');
10331  });
10332  
10333  // Force a reflow after toggling the class to ensure CSS recalculates
10334  void listItem.offsetHeight;
10335  
10336    // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal
10337    // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout
10338    // Use contains() instead of closest() because closest() only checks ancestors
10339    const galleryTree = document.getElementById('galleryTree');
10340    const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10341    const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10342    // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10342-container AND not in galleryTree
10343    const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar;
10344    
10345    let nestedList = null;
10346    
10347    // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures
10348    // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar
10349    if (isInGalleryTree) {
10350      nestedList = listItem.querySelector('ol.nested-sortable');
10351      
10352      // Also check direct children as fallback
10353      if (!nestedList) {
10354        const directChildren = Array.from(listItem.children);
10355        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10356      }
10357    } else if (isInSidebar) {
10358      // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar
10359      nestedList = listItem.querySelector('ol.nested-sortable');
10360      if (!nestedList) {
10361        const directChildren = Array.from(listItem.children);
10362        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10363      }
10364    }
10365    
10366    // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree)
10367    if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) {
10368      nestedList = listItem.querySelector('.horizontal-submenu');
10369      if (!nestedList) {
10370        nestedList = listItem.querySelector('.horizontal-dropdown');
10371      }
10372      if (!nestedList) {
10373        // For horizontal nested dropdowns, they're appended to document.body, not the list item
10374        // They're linked via data-flyout-id attribute
10375        const flyoutId = listItem.getAttribute('data-flyout-id');
10376        if (flyoutId) {
10377          // Look specifically for div.horizontal-nested-dropdown, not the list item itself
10378          const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]');
10379          
10380          // Find the actual dropdown div, not the list item
10381          const flyout = Array.from(allElementsWithId).find(el => 
10382            el.classList.contains('horizontal-nested-dropdown') && el !== listItem
10383          );
10384          
10385          
10386          if (flyout) {
10387            nestedList = flyout.querySelector('.horizontal-nested-list') || flyout;
10388          }
10389        }
10390      }
10391      if (!nestedList) {
10392        nestedList = listItem.querySelector('.horizontal-nested-dropdown');
10393      }
10394    }
10395    
10396    // Fallback: try all selectors if not found yet
10397    if (!nestedList) {
10398      nestedList = listItem.querySelector('ol.nested-sortable');
10399    }
10400    if (!nestedList) {
10401      // Debug: Log the actual DOM structure to understand what's there
10402      
10403      // Try to find any nested structure at all
10404      const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]');
10405      if (anyNested) {
10406        nestedList = anyNested; // Use it anyway
10407      }
10408      
10409      // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector
10410      // This can happen if the element was removed/re-added or if there's a timing issue
10411      const directChildren = Array.from(listItem.children);
10412      const olChild = directChildren.find(child => child.tagName === 'OL');
10413      if (olChild) {
10414        nestedList = olChild;
10415      }
10416    }
10417    
10418    if (!nestedList) {
10419      
10420      // If the nested list doesn't exist, we need to create it
10421      // This can happen if children aren't rendered until the folder is opened
10422      // First try flat array
10423      let gallery = galleries.find(g => g.id === id);
10424      
10425      // If gallery not found or has no children in flat array, build tree structure to find it
10426      if (!gallery || !gallery.children || gallery.children.length === 0) {
10427        
10428        // Build tree structure from flat array
10429        const buildTree = (items, parentId = null) => {
10430          return items
10431            .filter(item => item.parentId === parentId)
10432            .map(item => ({
10433              ...item,
10434              children: buildTree(items, item.id)
10435            }));
10436        };
10437        
10438        const galleryTree = buildTree(galleries);
10439        
10440        // Recursive function to find gallery in tree
10441        const findInTree = (items) => {
10442          for (const item of items) {
10443            if (item.id === id) {
10444              return item;
10445            }
10446            if (item.children && item.children.length > 0) {
10447              const found = findInTree(item.children);
10448              if (found) return found;
10449            }
10450          }
10451          return null;
10452        };
10453        
10454        const treeGallery = findInTree(galleryTree);
10455        if (treeGallery) {
10456          gallery = treeGallery;
10457        }
10458      }
10459      
10460        if (gallery && gallery.children && gallery.children.length > 0) {
10461          
10462          // Determine nesting level from list item
10463          const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10);
10464          const nextLevel = currentLevel + 1;
10465          
10466          // Create the nested list element
10467          nestedList = document.createElement('ol');
10468          nestedList.id = 'gallery-children-' + id;
10469          nestedList.className = 'nested-sortable';
10470          if (nextLevel > 0) {
10471            nestedList.classList.add('nested-level-' + nextLevel);
10472          }
10473          
10474          // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10475          gallery.children.forEach(child => {
10476            const childHtml = renderGalleryItem(child, nextLevel, 20);
10477            const tempDiv = document.createElement('div');
10478            tempDiv.innerHTML = childHtml;
10479            const childLi = tempDiv.querySelector('li');
10480            if (childLi) {
10481              nestedList.appendChild(childLi);
10482            }
10483          });
10484          
10485          // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context
10486          // If we're in #galleryTree (sidebar), skip all horizontal menu logic
10487          const galleryTree = document.getElementById('galleryTree');
10488          const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10489          const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10490          
10491          if (!isInGalleryTree && !isInSidebar) {
10492            const isInHorizontalMenu = listItem.closest('.horizontal-menu-container');
10493            if (isInHorizontalMenu) {
10494              const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown');
10495              if (horizontalDropdown && horizontalDropdown !== listItem) {
10496                horizontalDropdown.style.setProperty('display', 'block', 'important');
10497                horizontalDropdown.style.setProperty('visibility', 'visible', 'important');
10498                horizontalDropdown.style.setProperty('opacity', '1', 'important');
10499              }
10500            }
10501          }
10502          
10503          // CRITICAL: Ensure list item itself is visible and has correct display
10504          listItem.style.setProperty('display', 'list-item', 'important');
10505          listItem.style.setProperty('visibility', 'visible', 'important');
10506          
10507          // CRITICAL: Ensure ALL parent elements up to the sidebar are visible
10508          // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu!
10509          let parent = listItem.parentElement;
10510          let depth = 0;
10511          while (parent && depth < 10) {
10512            // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10513            if (parent.classList.contains('horizontal-menu-container')) {
10514              parent = parent.parentElement;
10515              depth++;
10516              continue;
10517            }
10518            
10519            const parentStyles = window.getComputedStyle(parent);
10520            if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10521              if (parentStyles.display === 'none') {
10522                parent.style.setProperty('display', 'block', 'important');
10523              }
10524              if (parentStyles.visibility === 'hidden') {
10525                parent.style.setProperty('visibility', 'visible', 'important');
10526              }
10527              if (parentStyles.opacity === '0') {
10528                parent.style.setProperty('opacity', '1', 'important');
10529              }
10530            }
10531            // Stop at sidebar or galleryTree
10532            if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') {
10533              break;
10534            }
10535            parent = parent.parentElement;
10536            depth++;
10537          }
10538          
10539          // Append directly to the list item (not to any wrapper)
10540          // Make sure we're not appending to a hidden parent
10541          listItem.appendChild(nestedList);
10542          
10543          // Immediately set the nested list to be visible since we're opening it
10544          nestedList.style.setProperty('display', 'block', 'important');
10545          nestedList.style.setProperty('visibility', 'visible', 'important');
10546          nestedList.style.setProperty('opacity', '1', 'important');
10547          nestedList.style.setProperty('height', 'auto', 'important');
10548          nestedList.style.setProperty('max-height', 'none', 'important');
10549          
10550          // Force immediate reflow after appending
10551          void nestedList.offsetHeight;
10552          void listItem.offsetHeight;
10553        }
10554    }
10555    
10556    if (nestedList) {
10557    if (!gallery.isCollapsed) {
10558      // Show the nested list - ensure parent has expanded class FIRST
10559      // CRITICAL: Add expanded class BEFORE any style operations
10560      listItem.classList.add('expanded');
10561      
10562      // Verify expanded class was added
10563      if (!listItem.classList.contains('expanded')) {
10564        listItem.className = listItem.className + ' expanded';
10565      }
10566      
10567      // CRITICAL: Ensure parent li is visible (it might be hidden by CSS)
10568      const parentVisibility = window.getComputedStyle(listItem).visibility;
10569      if (parentVisibility === 'hidden') {
10570        listItem.style.setProperty('visibility', 'visible', 'important');
10571      }
10572      
10573      // Force a reflow to ensure CSS recalculates after adding expanded class
10574      void listItem.offsetHeight;
10575      void nestedList.offsetHeight;
10576      
10577      // Show the nested list with !important to override CSS
10578      // Set these BEFORE removing any existing inline styles to avoid flicker
10579      nestedList.style.setProperty('display', 'block', 'important');
10580      nestedList.style.setProperty('visibility', 'visible', 'important');
10581      nestedList.style.setProperty('opacity', '1', 'important');
10582      nestedList.style.setProperty('height', 'auto', 'important');
10583      nestedList.style.setProperty('max-height', 'none', 'important');
10584      nestedList.style.setProperty('overflow', 'visible', 'important');
10585      
10586      // Also ensure the nested list is not clipped by parent overflow
10587      // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu!
10588      let checkParent = nestedList.parentElement;
10589      let checkDepth = 0;
10590      while (checkParent && checkDepth < 5) {
10591        // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu
10592        if (checkParent.classList.contains('horizontal-menu-container')) {
10593          checkParent = checkParent.parentElement;
10594          checkDepth++;
10595          continue;
10596        }
10597        
10598        const parentOverflow = window.getComputedStyle(checkParent).overflow;
10599        if (parentOverflow === 'hidden') {
10600          checkParent.style.setProperty('overflow', 'visible', 'important');
10601        }
10602        if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') {
10603          break;
10604        }
10605        checkParent = checkParent.parentElement;
10606        checkDepth++;
10607      }
10608      
10609      // Force another reflow after setting styles
10610      void nestedList.offsetHeight;
10611      
10612      const finalRect = nestedList.getBoundingClientRect();
10613      
10614      // If the nested list has zero dimensions, it's not actually visible
10615      if (finalRect.width === 0 || finalRect.height === 0) {
10616        // Force all parent elements to be visible
10617        // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu!
10618        let forceParent = nestedList.parentElement;
10619        while (forceParent && forceParent !== document.body) {
10620          // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10621          if (forceParent.classList.contains('horizontal-menu-container')) {
10622            forceParent = forceParent.parentElement;
10623            continue;
10624          }
10625          
10626          forceParent.style.setProperty('display', 'block', 'important');
10627          forceParent.style.setProperty('visibility', 'visible', 'important');
10628          forceParent.style.setProperty('opacity', '1', 'important');
10629          forceParent.style.setProperty('overflow', 'visible', 'important');
10630          if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') {
10631            break;
10632          }
10633          forceParent = forceParent.parentElement;
10634        }
10635        void nestedList.offsetHeight;
10636      }
10637      
10638      // Check for any parent elements that might be hiding the nested list and fix them
10639      let parent = nestedList.parentElement;
10640      let depth = 0;
10641      while (parent && depth < 5) {
10642        const parentStyles = window.getComputedStyle(parent);
10643        if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10644          
10645          // Fix hidden parent elements
10646          if (parentStyles.display === 'none') {
10647            parent.style.setProperty('display', 'block', 'important');
10648          }
10649          if (parentStyles.visibility === 'hidden') {
10650            parent.style.setProperty('visibility', 'visible', 'important');
10651          }
10652          if (parentStyles.opacity === '0') {
10653            parent.style.setProperty('opacity', '1', 'important');
10654          }
10655        }
10656        parent = parent.parentElement;
10657        depth++;
10658      }
10659      
10660    } else {
10661      // Hide the nested list
10662      nestedList.style.setProperty('display', 'none', 'important');
10663      nestedList.style.setProperty('visibility', 'hidden', 'important');
10664      nestedList.style.setProperty('opacity', '0', 'important');
10665    }
10666    
10667    // Force multiple reflows to ensure the browser applies all changes
10668    void listItem.offsetHeight;
10669    void nestedList.offsetHeight;
10670    void listItem.offsetWidth;
10671    void nestedList.offsetWidth;
10672    
10673    // Force a more aggressive repaint by temporarily toggling a class
10674    // This is especially important for sidebar layout where CSS rules might be interfer
10674ing
10675    listItem.classList.add('force-reflow');
10676    void listItem.offsetHeight;
10677    listItem.classList.remove('force-reflow');
10678    
10679    // Force reflow again after class manipulation
10680    void listItem.offsetHeight;
10681    void nestedList.offsetHeight;
10682    
10683    // Use requestAnimationFrame with multiple frames to ensure visual update
10684    // This ensures the browser has time to recalculate layout and repaint
10685    requestAnimationFrame(() => {
10686      void nestedList.offsetHeight;
10687      // Second frame to ensure all CSS has been recalculated
10688      requestAnimationFrame(() => {
10689        void nestedList.offsetHeight;
10690        // Verify the final state
10691        const finalDisplay = window.getComputedStyle(nestedList).display;
10692        const finalVisibility = window.getComputedStyle(nestedList).visibility;
10693        
10694        // If still not visible, force one more repaint cycle
10695        if (!gallery.isCollapsed && finalDisplay === 'none') {
10696          nestedList.style.setProperty('display', 'block', 'important');
10697          nestedList.style.setProperty('visibility', 'visible', 'important');
10698          void nestedList.offsetHeight;
10699          requestAnimationFrame(() => {
10700            void nestedList.offsetHeight;
10701          });
10702        }
10703      });
10704    });
10705    }
10706  
10707  // Find the submenu toggle icon and rotate it
10708  const toggleIcon = listItem.querySelector('.toggle-icon');
10709  if (toggleIcon) {
10710    toggleIcon.classList.toggle('rotated', !gallery.isCollapsed);
10711  }
10712  
10713  
10714  // Folder open/closed is a local UI preference — do not POST the full site config.
10715  try {
10716    const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}');
10717    folderStates[id] = !gallery.isCollapsed;
10718    localStorage.setItem('folder_states', JSON.stringify(folderStates));
10719  } catch (e) {
10720    console.error('Error saving folder state to localStorage:', e);
10721  }
10722}
10723
10724window.toggleFolder = toggleFolder;
10725
10726/**
10727 * Toggle password protection for a gallery/page
10728 * @param {number} id - The gallery ID
10729 * @param {Event} event - The click event
10730 */
10731function toggleGalleryPasswordProtection(id, event) {
10732  event.stopPropagation();
10733  
10734  const gallery = findGalleryById(galleries, id);
10735  if (!gallery) {
10736    console.error("Could not find gallery for password protection toggle, ID:", id);
10737    return;
10738  }
10739
10740  if (gallery.passwordProtected) {
10741    // Remove password protection
10742    if (confirm('Remove password protection from this page?')) {
10743      gallery.passwordProtected = false;
10744      delete gallery.password;
10745      saveGalleries();
10746      renderGalleries();
10747    }
10748  } else {
10749    // Add password protection
10750    const password = prompt('Enter a password for this page:');
10751    if (password && password.trim()) {
10752      gallery.passwordProtected = true;
10753      gallery.password = password.trim();
10754      saveGalleries();
10755      renderGalleries();
10756    }
10757  }
10758}
10759
10760window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection;
10761
10762function onPageMenuChromeToggle(checkbox) {
10763  if (!checkbox || !checkbox.checked) return;
10764  const root = checkbox.closest ? checkbox.closest('li') : null;
10765  if (!root) return;
10766  const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu';
10767  const sibling = root.querySelector(siblingClass);
10768  if (sibling && sibling !== checkbox) sibling.checked = false;
10769}
10770window.onPageMenuChromeToggle = onPageMenuChromeToggle;
10771
10772/**
10773 * Saves the edited gallery item details.
10774 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic)
10775 * @param {number} id - The ID of the gallery item.
10776 * @param {Event} event - The click event.
10777 */
10778function saveEdit(id, event) {
10779  event.stopPropagation();
10780  const li = getGalleryTreeListItem(id, event);
10781  if (!li) {
10782    console.error("Could not find list item for saveEdit, ID:", id);
10783    return;
10784  }
10785
10786  const titleInput = li.querySelector('.edit-title');
10787  const slugInput = li.querySelector('.edit-slug');
10788  const externalUrlInput = li.querySelector('.edit-external-url');
10789  const metaTitleInput = li.querySelector('.edit-meta-title');
10790  const metaDescriptionInput = li.querySelector('.edit-meta-description');
10791  const hideMenuInput = li.querySelector('.edit-hide-menu');
10792  const overlayMenuInput = li.querySelector('.edit-overlay-menu');
10793  const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent');
10794  const passwordProtectedInput = li.querySelector('.edit-password-protected');
10795  const passwordInput = li.querySelector('.edit-password');
10796  const noIndexInput = li.querySelector('.edit-no-index');
10797
10798  const title = titleInput ? titleInput.value.trim() : '';
10799  const newSlugRaw = slugInput ? slugInput.value.trim() : null;
10800  const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : '';
10801  const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : '';
10802  const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : '';
10803  const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10804  const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false;
10805  const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false;
10806  const password = passwordInput ? passwordInput.value.trim() : '';
10807  // noIndex is stored explicitly so it survives visibility changes without reverting
10808  const noIndex = noIndexInput ? noIndexInput.checked : undefined;
10809
10810  const gallery = findGalleryById(galleries, id);
10811  if (!gallery) {
10812    console.error("Could not find gallery object for saveEdit, ID:", id);
10813    renderGalleries();
10814    return;
10815  }
10816
10817  if (!title && !gallery.isSpacer) {
10818    alert('Please enter a title');
10819    return;
10820  }
10821
10822  // Validate password protection
10823  if (passwordProtected && !password) {
10824    alert('Please enter a password when password protection is enabled');
10825    return;
10826  }
10827
10828  // Only mutate the opt-in property when the user explicitly changes this checkbox.
10829  // This preserves the serialized shape and behavior of every existing item.
10830  if (actsAsMenuParentInput) {
10831    const wasNavigableMenuParent = gallery.actsAsMenuParent === true;
10832    const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true;
10833    if (wasNavigableMenuParent && !wantsNavigableMenuParent) {
10834      const hasNestedItems = galleries.some(item => item.parentId === gallery.id);
10835      if (hasNestedItems) {
10836        alert('Move all nested menu items out of this item before turning off “Allow nested menu items”.');
10837        return;
10838      }
10839      delete gallery.actsAsMenuParent;
10840    } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) {
10841      gallery.actsAsMenuParent = true;
10842    }
10843  }
10844
10845  gallery.title = title;
10846  // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page.
10847  if (!gallery.isPage) {
10848    gallery.metaTitle = metaTitle;
10849    gallery.metaDescription = metaDescription;
10850  }
10851  // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off.
10852  if (hideMenu) {
10853    gallery.hideMenuOnPage = true;
10854    delete gallery.overlayMenuOnPage;
10855  } else {
10856    gallery.hideMenuOnPage = hideMenu;
10857    if (overlayMenu) {
10858      gallery.overlayMenuOnPage = true;
10859    } else {
10860      delete gallery.overlayMenuOnPage;
10861    }
10862  }
10863  // Persist the explicit noIndex choice so it survives future visibility toggles
10864  if (noIndex !== undefined) gallery.noIndex = noIndex;
10865
10866  // Save password protection settings
10867  gallery.passwordProtected = passwordProtected;
10868  if (passwordProtected) {
10869    gallery.password = password;
10870  } else {
10871    delete gallery.password; // Remove password if protection is disabled
10872  }
10873
10874  if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) {
10875    const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10876    let sanitizedSlug = slugify(newSlugRaw);
10877
10878    if (!sanitizedSlug) {
10879      sanitizedSlug = slugify(gallery.title);
10880    }
10881
10882    if (sanitizedSlug !== originalSlug) {
10883      const otherGalleries = galleries.filter(g => g.id !== id);
10884      gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries);
10885      gallery.url = `/${gallery.slug}`;
10886    } else {
10887      if (gallery.url !== `/${gallery.slug}`) {
10888        gallery.url = `/${gallery.slug}`;
10889      }
10890    }
10891  }
10892
10893  // Update external URL if this is an external link
10894  if (gallery.isExternal && externalUrl) {
10895    gallery.url = externalUrl;
10896  }
10897
10898  saveGalleries();
10899  renderGalleries();
10900
10901  // If the currently active gallery was just edited, re-apply menu visibility
10902    if (gallery.id === activeGalleryId) {
10903    const bodyEl = document.body;
10904    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
10905        bodyEl.classList.add('menu-hidden-on-page');
10906    } else {
10907        bodyEl.classList.remove('menu-hidden-on-page');
10908    }
10909    applyMenuOverlayClassForGallery(gallery);
10910  }
10911}
10912// Helper function to check if a gallery is an ancestor of another
10913function hasAncestor(galleryId, ancestorId) {
10914if (galleryId === ancestorId) return true;
10915
10916const gallery = galleries.find(g => g.id === galleryId);
10917if (!gallery || !gallery.parentId) return false;
10918
10919if (gallery.parentId === ancestorId) return true;
10920
10921return hasAncestor(gallery.parentId, ancestorId);
10922}
10923
10924function deleteGallery(id, event) {
10925  event.stopPropagation();
10926  const gallery = galleries.find(g => g.id === id);
10927  const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item';
10928  if (!confirm('Are you sure you want to delete "' + title + '"?')) {
10929    return;
10930  }
10931  const overlay = showDeletionOverlay();
10932  overlay.updateMessage('Saving...');
10933  galleries = galleries.filter(g => g.id !== id);
10934  if (typeof window !== 'undefined') {
10935    window.galleries = galleries;
10936  }
10937  if (activeGalleryId === id) {
10938    activeGalleryId = null;
10939    const frame = document.getElementById('galleryFrame');
10940    if (frame) frame.src = '';
10941  }
10942  saveGalleries()
10943    .then(function(success) {
10944      renderGalleries();
10945      overlay.hide();
10946      if (success === false) {
10947        alert('Failed to delete. Please try again.');
10948      }
10949    })
10950    .catch(function(err) {
10951      overlay.hide();
10952      console.error('Error deleting item:', err);
10953      alert('Failed to delete. Please try again.');
10954    });
10955}
10956
10957function debugNestedStructure() {
10958console.group('Current Gallery Structure');
10959
10960function printGallery(gallery, level = 0) {
10961const indent = '  '.repeat(level);
10962console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`);
10963
10964if (gallery.children && gallery.children.length > 0) {
10965  gallery.children.forEach(child => printGallery(child, level + 1));
10966}
10967}
10968  // Create a tree structure for debugging
10969const galleryTree = createGalleryTree(galleries);
10970galleryTree.forEach(gallery => printGallery(gallery));
10971
10972console.groupEnd();
10973}
10974
10975function toggleHome(id, event) {
10976  event.stopPropagation();
10977  
10978  console.log('Setting home page to gallery:', id);
10979  
10980  // Find gallery
10981  const gallery = galleries.find(g => g.id === id);
10982  if (!gallery) return;
10983  
10984  // Check if this gallery is already set as home
10985  const alreadyHome = gallery.isHomePage === true;
10986  
10987  // Set all galleries to not be the home page
10988  galleries.forEach(g => {
10989    g.isHomePage = false;
10990  });
10991  
10992  // Toggle this gallery's home status - if it was already home, we're unsetting it
10993  gallery.isHomePage = !alreadyHome;
10994  
10995  // Save changes
10996  saveGalleries();
10997  
10998  // Update UI
10999  renderGalleries();
11000  
11001  // If this gallery is now the home page, display a message
11002  if (gallery.isHomePage) {
11003    const title = gallery.title || 'This page';
11004    showSuccessMessage(`${title} is now set as the Home page`);
11005  }
11006}
11007
11008// Function to toggle gallery visibility
11009function toggleVisibility(id, event) {
11010  event.stopPropagation();
11011  
11012  const gallery = galleries.find(g => g.id === id);
11013  if (gallery) {
11014    // Toggle the visibility
11015    gallery.visible = gallery.visible === false ? true : false;
11016    
11017    // Save the change to server
11018    saveGalleries();
11019    
11020    // Update UI - important to toggle the class on the button itself
11021    const toggle = event.currentTarget;
11022    if (toggle) {
11023      if (gallery.visible === false) {
11024        toggle.classList.add('hidden');
11025        
11026        // Update the SVG icon
11027        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>';
11028      } else {
11029        toggle.classList.remove('hidden');
11030        
11031        // Update the SVG icon
11032        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>';
11033      }
11034    }
11035    
11036    // Update parent row class (#galleryTree row, not horizontal submenu li)
11037    const listItem = getGalleryTreeListItem(id, event);
11038    if (listItem) {
11039      if (gallery.visible === false) {
11040        listItem.classList.add('hidden-gallery');
11041      } else {
11042        listItem.classList.remove('hidden-gallery');
11043      }
11044    }
11045    
11046    // Only clear iframe if not in edit mode
11047    if (id === activeGalleryId && gallery.visible === false && !isEditing) {
11048      document.getElementById('galleryFrame').src = '';
11049    }
11050    
11051    // Re-render menus based on current layout
11052    const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 
11053                         document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar';
11054    
11055    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
11056      window.renderHorizontalMenu();
11057    }
11058    
11059    // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied
11060    // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode
11061    if (typeof window.renderGalleries === 'function') {
11062      window.renderGalleries();
11063    } else if (typeof renderGalleries === 'function') {
11064      renderGalleries();
11065    }
11066  }
11067}
11068
11069
11070
11071/**
11072* Simplified loadGallery function that follows the same content swap pattern
11073* @param {number} id - The gallery ID
11074* @param {Event} event - Optional event object
11075*/
11076// Enhanced loadGallery function with script cleanup
11077async function loadGallery(id, event) {
11078
11079// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation
11080if (event) {
11081  if (typeof event.preventDefault === 'function') event.preventDefault();
11082  event.stopPropagation();
11083}
11084
11085const galleryContainer = document.querySelector('.gallery-container');
11086if (galleryContainer) {
11087  // Apply immediate hiding styles
11088  galleryContainer.style.opacity = '0';
11089}
11090
11091console.log('Loading gallery with ID:', id);
11092
11093if (typeof dismissGalleryPasswordOverlay === 'function') {
11094  dismissGalleryPasswordOverlay();
11095}
11096
11097// Find gallery by ID
11098const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
11099
11100if (!gallery) {
11101  console.warn('No gallery found with ID:', id);
11102  if (galleryContainer) {
11103  galleryContainer.style.opacity = '1'; 
11104  } 
11105  return;
11106}
11107
11108// Check if this is a page, if so use loadPage instead
11109if (isPageMenuItem(gallery)) {
11110  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
11111  return loadPage(id, event);
11112}
11113
11114// Skip submenu items
11115if (gallery.isSubmenu) {
11116  console.log('Gallery is submenu, skipping URL update');
11117  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
11118  if (galleryContainer) {
11119  galleryContainer.style.opacity = '1';
11120  } // Show container
11121  return;
11122}
11123
11124console.log('Found gallery:', gallery.title);
11125
11126window._activeContentType = 'gallery';
11127beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
11128if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
11129  window.PageManager.clearCurrentPage();
11130}
11131
11132// CRITICAL: Set active gallery ID consistently in both global and window scope
11133console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
11134window.activeGalleryId = id;
11135activeGalleryId = id; // Ensure both variables are synchronized
11136
11137// Save reference to which gallery we're loading (for later verification)
11138window.currentLoadingGalleryId = id;
11139
11140// CRITICAL: Thorough cleanup with await to ensure completion
11141if (typeof window.removeGalleryScriptsWithPause === 'function') {
11142  await window.removeGalleryScriptsWithPause();
11143}
11144
11145
11146// Clear all existing content from gallery container
11147if (!galleryContainer) {
11148  console.error('Gallery container not found');
11149  return;
11150}
11151
11152// Force clear all content
11153galleryContainer.innerHTML = '';
11154
11155
11156  // CRITICAL: Double-check active gallery ID before updating UI
11157  if (window.activeGalleryId !== id || activeGalleryId !== id) {
11158    console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
11159    window.activeGalleryId = id;
11160    activeGalleryId = id;
11161  }
11162
11163// Apply menu visibility based on the gallery's setting and edit mode
11164const bodyEl = document.body;
11165if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
11166    bodyEl.classList.add('menu-hidden-on-page');
11167} else {
11168    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
11169}
11170applyMenuOverlayClassForGallery(gallery);
11171
11172// Update UI state using explicit window function references or local fallbacks
11173if (typeof window.updateActiveStates === 'function') {
11174  window.updateActiveStates();
11175} else if (typeof updateActiveStates === 'function') {
11176  updateActiveStates();
11177}
11178
11179if (typeof window.updateMobileTitle === 'function') {
11180  window.updateMobileTitle();
11181} else if (typeof updateMobileTitle === 'function') {
11182  updateMobileTitle();
11183}
11184
11185if (typeof window.closeMobileMenu === 'function') {
11186  window.closeMobileMenu();
11187} else if (typeof closeMobileMenu === 'function') {
11188  closeMobileMenu();
11189}
11190
11191// Update URL with gallery slug
11192if (typeof window.updateURLWithGallerySlug === 'function') {
11193  window.updateURLWithGallerySlug(gallery);
11194} else if (typeof updateURLWithGallerySlug === 'function') {
11195  updateURLWithGallerySlug(gallery);
11196}
11197
11198  // Create gallery container and load content
11199const neonGalleryContainer = document.createElement('div');
11200neonGalleryContainer.id = 'neon-gallery-container';
11201neonGalleryContainer.className = 'gallery-direct-content';
11202neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
11203neonGalleryContainer.style.width = '100%';
11204neonGalleryContainer.style.height = '100%';
11205galleryContainer.appendChild(neonGalleryContainer);
11206
11207// Add a cache-busting parameter to ensure script is freshly loaded
11208const timestamp = Date.now();
11209
11210// Set up gallery parameters
11211window.Parameters = {
11212  SiteAlias: window.location.hostname,
11213  InitialPageUuid: gallery.pageId,
11214  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
11215  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
11216  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11217  isHydra: true,
11218  galleryInstanceId: timestamp,
11219  // Added loadedGalleryId as per your original function
11220  loadedGalleryId: gallery.id
11221};
11222
11223// Debug: Check what galleryOptions contains
11224console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
11225
11226// Set up gallery config
11227window.neonGalleryConfig = {
11228  useData: true,
11229  useCDN: true,
11230  version: 'live',
11231  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
11232  layoutType: gallery.galleryOptions?.layoutType || "grid",
11233  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
11234  // This ensures that only properties from the current gallery are included, not from previous galleries
11235  ...(gallery.galleryOptions || {}),
11236  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
11237  // These fields are required for the gallery to determine if it should use Load Network racing
11238  loadTxId: gallery.galleryOptions?.loadTxId || null,
11239  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
11240  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
11241  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
11242  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
11243  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
11244  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
11245  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
11246  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11247  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
11248  galleryId: gallery.id,
11249  galleryInstanceId: timestamp
11250};
11251
11252console.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);
11253
11254// Create and add the gallery script with cache-busting
11255// Use Worker route (avoids CDN SSL issues)
11256const script = document.createElement('script');
11257const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
11258script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
11259script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
11260script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
11261console.log('Loading gallery-main script from Worker:', script.src);
11262script.onerror = function() {
11263  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
11264  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
11265  script.src = proxyUrl;
11266  script.onerror = function() {
11267    console.error('❌ Proxy failed, trying storage:', proxyUrl);
11268    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
11269  };
11270};
11271
11272// Added script.onload from your original function
11273script.onload = function() {
11274  if (window.currentLoadingGalleryId !== gallery.id) {
11275    console.warn('Gallery ID mismatch! Expected:', gallery.id,
11276                 'Current:', window.currentLoadingGalleryId);
11277  }
11278  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
11279};
11280
11281
11282if (galleryContainer) {
11283    setTimeout(() => {
11284      galleryContainer.style.opacity = '1';
11285    }, 50); // Small delay to ensure content is ready
11286  }
11287
11288console.log(`Loading fresh gallery script for: ${gallery.title}`);
11289document.body.appendChild(script);
11290}
11291
11292
11293
11294function removeGalleryScriptsWithPause() {
11295  return new Promise(resolve => {
11296    console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)');
11297    
11298    // 1. Capture and clear any gallery data in localStorage (remains the same)
11299    try {
11300      const localStorageKeys = Object.keys(localStorage);
11301      const galleryStorageKeys = localStorageKeys.filter(key => 
11302        key.includes('gallery') || 
11303        key.includes('neon') || 
11304        key.includes('lightbox')
11305      );
11306      galleryStorageKeys.forEach(key => {
11307        localStorage.removeItem(key);
11308      });
11309    } catch (e) {
11310      console.warn('Error clearing localStorage:', e);
11311    }
11312    
11313    // @ts-ignore
11314    window._galleryPageContext = null; // (remains the same)
11315    
11316    // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload;
11317    // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins.
11318    const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11319    galleryStyles.forEach(style => {
11320      const href = (style && style.href) ? String(style.href) : '';
11321      if (style && style.id === 'neon-gallery-css') return;
11322      if (href.indexOf('neon-gallery-hp-hydra') !== -1) return;
11323      if (style && style.parentNode) {
11324        style.parentNode.removeChild(style);
11325      }
11326    });
11327    
11328    // 4. Remove lightbox elements (more specific selectors now)
11329    // These are specific IDs and classes for lightbox components.
11330    const lightboxComponentSelectors = [
11331      '#neon-lightbox',      // Main lightbox container by ID
11332      '.neon-lightbox',     // Alternative class for main container
11333      '.lightbox-container',// General container class if used
11334      // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements
11335    ];
11336    const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', '));
11337    lightboxElements.forEach(lightboxEl => {
11338      if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) {
11339        // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className);
11340        lightboxEl.parentNode.removeChild(lightboxEl);
11341      }
11342    });
11343    
11344    // 5. Remove other gallery-specific DOM elements
11345    // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list
11346    // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body.
11347    const galleryElementSelectors = [
11348      '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay',
11349      '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu',
11350      '.neon-thumbnails', '.neon-image', '.neon-caption',
11351      '.neon-controls', '.neon-pagination',
11352      '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues.
11353      '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry'
11354      // Ensure none of these selectors inadvertently match document.body or critical layout containers
11355    ];
11356    const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', '));
11357    
11358    if (galleryElements.length > 0) {
11359      galleryElements.forEach(el => {
11360        // CRITICAL FIX: Add checks to ensure we don't remove body or html
11361        if (el && el.parentNode && el !== document.body && el !== document.documentElement) {
11362          // console.log('Removing gallery-specific DOM element:', el.id || el.className);
11363          el.parentNode.removeChild(el);
11364        } else if (el === document.body || el === document.documentElement) {
11365          console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`);
11366        }
11367      });
11368    }
11369    
11370    // 6. Find and remove ALL gallery scripts (remains the same)
11371    const scripts = document.querySelectorAll(
11372      'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11373      'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11374    );
11375    scripts.forEach(script => {
11376      if (script && script.parentNode) {
11377        // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id'));
11378        script.parentNode.removeChild(script);
11379      }
11380    });
11381
11382    // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries)
11383    const optionsScripts = document.querySelectorAll(
11384      'script[src*="galleryOptions"], script[data-options-js]'
11385    );
11386    optionsScripts.forEach(script => {
11387      if (script && script.parentNode) {
11388        console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js'));
11389        script.parentNode.removeChild(script);
11390      }
11391    });
11392
11393    // NOTE: We intentionally do NOT remove gallery options CSS here
11394    // The CSS is needed across galleries and doesn't cause conflicts like scripts do
11395
11396    setTimeout(() => {
11397      const remainingScripts = document.querySelectorAll(
11398        'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11399        'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11400      );
11401      if (remainingScripts.length > 0) {
11402        // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`);
11403      }
11404    }, 50);
11405        
11406    // 7. Reset ALL known global state variables used by the gallery (remains the same)
11407    const resetGlobals = [
11408      'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
11409      'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState',
11410      'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings',
11411      'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance',
11412      'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache',
11413    ];
11414    
11415    resetGlobals.forEach(prop => {
11416      // @ts-ignore
11417      if (window[prop] !== undefined) {
11418        // @ts-ignore
11419        window[prop] = null;
11420      }
11421    });
11422    // Keep the bootstrap wipe flag. Clearing it let a re-injected main script
11423    // replace PageManager embed config with the default "mod" collection.
11424    if (typeof window.resetNeonGalleryInitGates === 'function') {
11425      window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause');
11426    }
11427
11428    // 7b. Reset gallery options panel state
11429    if (window._editorButtonVisibilityInterval) {
11430      clearInterval(window._editorButtonVisibilityInterval);
11431      window._editorButtonVisibilityInterval = null;
11432    }
11433    const editorBtn = document.getElementById('editorButton');
11434    if (editorBtn) {
11435      editorBtn.classList.add('gallery-options-force-hidden');
11436      editorBtn.style.setProperty('display', 'none', 'important');
11437      editorBtn.style.pointerEvents = '';
11438    }
11439    if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
11440      window.syncGalleryOptionsButtonVisibility();
11441    }
11442    // Reset optionsVisible flag if it exists on window (for cross-module access)
11443    // @ts-ignore
11444    if (window.optionsVisible !== undefined) {
11445      // @ts-ignore
11446      window.optionsVisible = false;
11447    }
11448    
11449    // Reset options-related globals
11450    const optionsGlobals = [
11451      'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 
11452      'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible'
11453    ];
11454    optionsGlobals.forEach(prop => {
11455      // @ts-ignore
11456      if (window[prop] !== undefined && typeof window[prop] !== 'function') {
11457        // Only reset non-function properties (like flags)
11458        // Functions should remain available
11459        // @ts-ignore
11460        if (prop === 'optionsVisible') {
11461          // @ts-ignore
11462          window[prop] = false;
11463        }
11464      }
11465    });
11466    
11467    // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever
11468    // after scripts it was waiting on were just removed from the DOM.
11469    if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
11470      window.EnhancedGallerySystem.resetInitializationState();
11471    }
11472
11473    // Reset PageManager page-embedded gallery tracking
11474    window.neonGalleryScriptLoaded = false;
11475    window.pageManagerGalleryInitBlocked = false;
11476    if (window.pageManagerGalleryContainers) {
11477      window.pageManagerGalleryContainers.clear();
11478    }
11479
11480    // Remove orphaned lightbox keyboard listeners (survive script tag removal)
11481    if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
11482      window.removeAllLightboxKeyboardListeners();
11483    } else if (window.currentLightboxKeyboardListener) {
11484      document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
11485      window.currentLightboxKeyboardListener = null;
11486      window.lightboxKeyboardListenerActive = false;
11487    }
11488
11489    // Short pause before resolving
11490    setTimeout(resolve, 150);
11491  });
11492}
11493
11494
11495// Fixed clearAddForm function with proper variable names
11496function clearAddForm() {
11497// Clear the title input
11498const titleInput = document.getElementById('galleryTitle');
11499if (titleInput) {
11500titleInput.value = '';
11501}
11502
11503// Clear the URL input
11504const urlInput = document.getElementById('galleryUrl');
11505if (urlInput) {
11506urlInput.value = '';
11507}
11508
11509// Reset the parent selection
11510const parentSelect = document.getElementById('galleryParent');
11511if (parentSelect) {
11512parentSelect.value = '';
11513}
11514
11515// Reset radio buttons to Gallery (integrated) as default
11516const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]');
11517if (galleryRadio) {
11518galleryRadio.checked = true;
11519}
11520
11521// Hide URL input container when default is Gallery
11522const urlInputContainer = document.getElementById('urlInputContainer');
11523if (urlInputContainer) {
11524urlInputContainer.style.display = 'none';
11525} else {
11526console.log('URL input container not found');
11527}
11528}
11529
11530/**
11531 * Ensures window.galleries is synchronized with the global galleries variable
11532 * Call this function after any modification to gallery settings
11533 * 
11534 * @returns {boolean} True if synchronization was successful
11535 */
11536/**
11537 * Enhanced synchronizeGalleries function that checks ALL galleries
11538 * not just imported ones, and ensures settings are preserved
11539 * @returns {boolean} Success indicator
11540 */
11541function synchronizeGalleries() {
11542  console.log('Starting enhanced gallery synchronization...');
11543  
11544  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
11545    // First check if window.galleries exists and is different from galleries
11546    const needsSync = !window.galleries || 
11547                     !Array.isArray(window.galleries) || 
11548                     window.galleries.length !== galleries.length;
11549    
11550    // ENHANCED: Check ALL galleries for options preservation, not just imported ones
11551    if (!needsSync && window.galleries.length > 0) {
11552      // Find all galleries with galleryOptions
11553      const galleriesWithOptions = galleries.filter(g => 
11554        g.galleryOptions && Object.keys(g.galleryOptions).length > 4
11555      );
11556      
11557      if (galleriesWithOptions.length > 0) {
11558        // Check ALL galleries with substantial options, not just a sample
11559        let syncNeeded = false;
11560        
11561        // Use for loop so we can break early if we find a mismatch
11562        for (let i = 0; i < galleriesWithOptions.length; i++) {
11563          const gallery = galleriesWithOptions[i];
11564          const windowGallery = window.galleries.find(g => g.id === gallery.id);
11565          
11566          if (windowGallery) {
11567            // Get options depth
11568            const galleryOptionsDepth = gallery.galleryOptions ? 
11569              Object.keys(gallery.galleryOptions).length : 0;
11570              
11571            const windowOptionsDepth = windowGallery.galleryOptions ? 
11572              Object.keys(windowGallery.galleryOptions).length : 0;
11573            
11574            // If options depth differs significantly, we need to sync
11575            if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) {
11576              console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`);
11577              console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`);
11578              
11579              syncNeeded = true;
11580              break; // No need to check more galleries
11581            }
11582            
11583            // Additional check: Look for specific important gallery settings that should be preserved
11584            // Only do this check for galleries with substantial settings
11585            if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) {
11586              // Critical keys that indicate gallery customization
11587              const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 
11588                                  'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor'];
11589              
11590              // Check if any critical keys are missing in window.galleries
11591              const missingKeys = criticalKeys.filter(key => 
11592                gallery.galleryOptions[key] !== undefined && 
11593                windowGallery.galleryOptions[key] === undefined
11594              );
11595              
11596              if (missingKeys.length > 0) {
11597                console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`);
11598                console.log(`Missing keys: ${missingKeys.join(', ')}`);
11599                
11600                syncNeeded = true;
11601                break; // No need to check more galleries
11602              }
11603            }
11604          }
11605        }
11606        
11607        // If we detected a need to sync, do it now
11608        if (syncNeeded) {
11609          // Ensure we preserve rich gallery settings and per-page custom HTML fields
11610          window.galleries = galleries.map(gallery => {
11611            const windowGallery = window.galleries.find(g => g.id === gallery.id);
11612            if (!windowGallery) return gallery;
11613
11614            let merged = { ...gallery };
11615
11616            if (windowGallery.galleryOptions && gallery.galleryOptions) {
11617              const globalOptionsDepth = Object.keys(gallery.galleryOptions).length;
11618              const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length;
11619              if (windowOptionsDepth > globalOptionsDepth) {
11620                merged = {
11621                  ...merged,
11622                  galleryOptions: {
11623                    ...windowGallery.galleryOptions,
11624                    ...gallery.galleryOptions
11625                  }
11626                };
11627              }
11628            }
11629
11630            if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11631              merged = {
11632                ...merged,
11633                customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11634                customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11635                customPageHtmlFilename: windowGallery.customPageHtmlFilename
11636              };
11637            }
11638
11639            return merged;
11640          });
11641          
11642          console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11643          return true;
11644        }
11645      }
11646    }
11647    
11648    // If we determined sync is needed (or as a precaution), update window.galleries
11649    if (needsSync || !window.galleries) {
11650      if (window.galleries && Array.isArray(window.galleries)) {
11651        galleries = galleries.map(function(gallery) {
11652          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11653          if (!windowGallery) return gallery;
11654          if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11655            return {
11656              ...gallery,
11657              customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11658              customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11659              customPageHtmlFilename: windowGallery.customPageHtmlFilename
11660            };
11661          }
11662          return gallery;
11663        });
11664      }
11665      window.galleries = [...galleries]; // Create a shallow copy to ensure different reference
11666      console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11667    } else {
11668      console.log('Gallery synchronization check: No sync needed');
11669    }
11670    
11671    // Verify that all galleries with rich settings have their full options preserved
11672    const richGalleries = galleries.filter(g => 
11673      g.galleryOptions && Object.keys(g.galleryOptions).length > 5
11674    );
11675    
11676    console.log(`Status: Found ${richGalleries.length} galleries with rich settings`);
11677    
11678    // Final verification - keep both arrays aligned without dropping page-level fields
11679    if (window.galleries !== galleries) {
11680      console.warn('References not identical after sync - merging page fields into global galleries');
11681      if (window.galleries && Array.isArray(window.galleries)) {
11682        galleries = galleries.map(function(gallery) {
11683          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11684          if (!windowGallery) return gallery;
11685          return {
11686            ...gallery,
11687            customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled,
11688            customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key,
11689            customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename,
11690            hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage,
11691            overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage
11692          };
11693        });
11694      }
11695      window.galleries = galleries;
11696      return true;
11697    }
11698    
11699    return true;
11700  } else {
11701    console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array');
11702    
11703    // If we have window.galleries but no global galleries, try to reverse-sync
11704    if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) {
11705      console.log('Attempting reverse sync: updating global galleries from window.galleries');
11706      try {
11707        // This is risky and should only happen in unusual circumstances
11708        galleries = window.galleries;
11709        return true;
11710      } catch (e) {
11711        console.error('Could not reverse-sync galleries:', e);
11712      }
11713    }
11714    
11715    return false;
11716  }
11717}
11718
11719
11720
11721/**
11722 * Loads NDJSON content from the original gallery
11723 * @param {string} siteId - The site ID
11724 * @param {string} pageId - The page ID of the original gallery
11725 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error
11726 */
11727async function loadGalleryNDJSON(siteId, pageId) {
11728  console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId);
11729  
11730  if (!siteId || !pageId) {
11731    console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId });
11732    return null;
11733  }
11734
11735  try {
11736    const ndjsonFilename = `${siteId}_${pageId}.ndjson`;
11737    const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`;
11738    
11739    console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url);
11740    console.error('[NDJSON Duplication] Filename:', ndjsonFilename);
11741    
11742    const response = await fetch(url);
11743    
11744    console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText);
11745    
11746    if (!response.ok) {
11747      if (response.status === 404) {
11748        console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet');
11749        console.error('[NDJSON Duplication] Attempted URL:', url);
11750        return null;
11751      }
11752      const errorText = await response.text().catch(() => 'Could not read error text');
11753      console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText);
11754      throw new Error(`Failed to load NDJSON: ${response.statusText}`);
11755    }
11756    
11757    const ndjsonContent = await response.text();
11758    console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters');
11759    console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200));
11760    return ndjsonContent;
11761  } catch (error) {
11762    console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error);
11763    console.error('[NDJSON Duplication] Error message:', error.message);
11764    console.error('[NDJSON Duplication] Error stack:', error.stack);
11765    return null;
11766  }
11767}
11768
11769/**
11770 * Duplicates and publishes NDJSON for a gallery
11771 * @param {Object} originalGallery - The original gallery object
11772 * @param {Object} newGallery - The new gallery object with new pageId
11773 * @returns {Promise<boolean>} - Success indicator
11774 */
11775async function duplicateGalleryNDJSON(originalGallery, newGallery) {
11776  console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON =====');
11777  console.error('[NDJSON Duplication] Original gallery:', {
11778    id: originalGallery.id,
11779    title: originalGallery.title,
11780    pageId: originalGallery.pageId,
11781    classicGuid: originalGallery.classicGuid,
11782    hasGalleryOptions: !!originalGallery.galleryOptions,
11783    manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11784    isClassicCollection: originalGallery.galleryOptions?.isClassicCollection
11785  });
11786  console.error('[NDJSON Duplication] New gallery:', {
11787    id: newGallery.id,
11788    title: newGallery.title,
11789    pageId: newGallery.pageId
11790  });
11791  
11792  try {
11793    // Check if this is an NDJSON-based gallery (not classic collection)
11794    // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files
11795    // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON)
11796    // Having a pageId means it uses NDJSON files, so we should duplicate them
11797    const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true;
11798    
11799    console.error('[NDJSON Duplication] Classic collection check:', {
11800      hasClassicGuid: !!originalGallery.classicGuid,
11801      manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11802      startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11802UID='),
11803      isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection,
11804      hasPageId: !!originalGallery.pageId,
11805      isClassicCollection: isClassicCollection,
11806      willSkip: isClassicCollection && !originalGallery.pageId
11807    });
11808    
11809    // Only skip if explicitly marked as classic collection AND has no pageId
11810    // If it has a pageId, it uses NDJSON files and we should duplicate them
11811    if (isClassicCollection && !originalGallery.pageId) {
11812      console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication');
11813      return true; // Not an error, just not applicable
11814    }
11815
11816    // Check if original gallery has a pageId
11817    if (!originalGallery.pageId) {
11818      console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication');
11819      console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery));
11820      return true; // Not an error, just not applicable
11821    }
11822
11823    // Get siteId
11824    const siteId = window.siteId || originalGallery.siteId;
11825    console.error('[NDJSON Duplication] SiteId resolution:', {
11826      windowSiteId: window.siteId,
11827      gallerySiteId: originalGallery.siteId,
11828      resolvedSiteId: siteId
11829    });
11830    
11831    if (!siteId) {
11832      console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId');
11833      console.error('[NDJSON Duplication] window.siteId:', window.siteId);
11834      console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId);
11835      return false;
11836    }
11837
11838    // Load original NDJSON
11839    console.error('[NDJSON Duplication] Loading original NDJSON for duplication...');
11840    console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId);
11841    const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId);
11842    
11843    if (!originalNdjson) {
11844      console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet');
11845      console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found');
11846      return false;
11847    }
11848
11849    // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher
11850    console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId);
11851    const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`;
11852    console.error('[NDJSON Duplication] New filename:', dataFileName);
11853    console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length);
11854    
11855    try {
11856      // Hydra sites require Bearer auth (same token resolution as Edition Publisher)
11857      const publishHeaders = { 'Content-Type': 'application/json' };
11858      let authToken = null;
11859      try {
11860        if (window.TokenManager) {
11861          if (typeof window.TokenManager.getHydraAuthToken === 'function') {
11862            try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
11863          }
11864          if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
11865            try {
11866              const tokenWithBearer = window.TokenManager.getBestToken();
11867              authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ')
11868                ? tokenWithBearer.substring(7)
11869                : tokenWithBearer;
11870            } catch (e) {}
11871          }
11872        }
11873        if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
11874        if (!authToken) authToken = window.authToken || window.didToken;
11875        if (authToken) {
11876          publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
11877            ? 'Bearer ' + authToken
11878            : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
11879          console.error('[NDJSON Duplication] Auth token attached for publish');
11880        } else {
11881          console.error('[NDJSON Duplication] WARNING: No auth token — publish will likely fail with 401');
11882        }
11883      } catch (authErr) {
11884        console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message);
11885      }
11886
11887      // Call the publish endpoint (same as edition publisher)
11888      console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish');
11889      const response = await fetch('https://hydra-press-v2.fly.dev/publish', {
11890        method: 'POST',
11891        headers: publishHeaders,
11892        body: JSON.stringify({
11893          ndjsonContent: originalNdjson,
11894          dataFileName: dataFileName
11895        }),
11896      });
11897      
11898      console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText);
11899      console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries()));
11900      
11901      if (!response.ok) {
11902        const errorText = await response.text().catch(() => 'Unknown error');
11903        console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText);
11904        console.error('[NDJSON Duplication] Response status:', response.status);
11905        console.error('[NDJSON Duplication] Response statusText:', response.statusText);
11906        return false;
11907      }
11908      
11909      const result = await response.json();
11910      console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON');
11911      console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2));
11912      console.error('[NDJSON Duplication] Publish result URLs:', result.urls);
11913      
11914      // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions
11915      // with txId/permaURL here. If Irys upload is needed, it would need to be done separately.
11916      
11917      console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY =====');
11918      return true;
11919    } catch (error) {
11920      console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error);
11921      console.error('[NDJSON Duplication] Error message:', error.message);
11922      console.error('[NDJSON Duplication] Error stack:', error.stack);
11923      return false;
11924    }
11925  } catch (error) {
11926    console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error);
11927    console.error('[NDJSON Duplication] Error message:', error.message);
11928    console.error('[NDJSON Duplication] Error stack:', error.stack);
11929    return false;
11930  }
11931}
11932
11933/**
11934 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication).
11935 * @returns {Object} Object with hide() and updateMessage() to control the overlay
11936 */
11937function showDeletionOverlay() {
11938    const existingOverlay = document.getElementById('deletion-overlay');
11939    if (existingOverlay) existingOverlay.remove();
11940    if (!document.querySelector('#deletion-overlay-styles')) {
11941        const style = document.createElement('style');
11942        style.id = 'deletion-overlay-styles';
11943        style.textContent = `
11944            #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; }
11945            #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; }
11946            #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; }
11947        `;
11948        document.head.appendChild(style);
11949    }
11950    const overlayEl = document.createElement('div');
11951    overlayEl.id = 'deletion-overlay';
11952    const messageEl = document.createElement('div');
11953    messageEl.className = 'deletion-message';
11954    const spinnerEl = document.createElement('div');
11955    spinnerEl.className = 'deletion-spinner';
11956    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>`;
11957    const textEl = document.createElement('div');
11958    textEl.textContent = 'Deleting...';
11959    textEl.style.textAlign = 'center';
11960    messageEl.appendChild(spinnerEl);
11961    messageEl.appendChild(textEl);
11962    overlayEl.appendChild(messageEl);
11963    document.body.appendChild(overlayEl);
11964    return {
11965        hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); },
11966        updateMessage: function(msg) { textEl.textContent = msg; }
11967    };
11968}
11969
11970/**
11971 * Creates and shows a loading overlay for gallery duplication
11972 * @returns {Object} Object with hide function to remove the overlay
11973 */
11974function showDuplicationOverlay() {
11975    // Remove any existing overlay first
11976    const existingOverlay = document.getElementById('duplication-overlay');
11977    if (existingOverlay) {
11978        existingOverlay.remove();
11979    }
11980    
11981    // Add styles if not already present
11982    if (!document.querySelector('#duplication-overlay-styles')) {
11983        const style = document.createElement('style');
11984        style.id = 'duplication-overlay-styles';
11985        style.textContent = `
11986            #duplication-overlay {
11987                position: fixed;
11988                top: 0;
11989                left: 0;
11990                right: 0;
11991                bottom: 0;
11992                background: rgba(0, 0, 0, 0.5);
11993                z-index: 99999999;
11994                display: flex;
11995                align-items: center;
11996                justify-content: center;
11997                cursor: not-allowed;
11998                pointer-events: all;
11999            }
12000            #duplication-overlay .duplication-message {
12001                background: rgba(255, 255, 255, 0.95);
12002                padding: 30px 50px;
12003                font-family: Arial, sans-serif;
12004                font-size: 16px;
12005                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
12006                display: flex;
12007                flex-direction: column;
12008                align-items: center;
12009                gap: 20px;
12010            }
12011            #duplication-overlay .duplication-spinner {
12012                width: 80px;
12013                height: 80px;
12014                color: #444444;
12015            }
12016        `;
12017        document.head.appendChild(style);
12018    }
12019    
12020    const overlay = document.createElement('div');
12021    overlay.id = 'duplication-overlay';
12022    
12023    const message = document.createElement('div');
12024    message.className = 'duplication-message';
12025    
12026    const spinner = document.createElement('div');
12027    spinner.className = 'duplication-spinner';
12028    spinner.innerHTML = `
12029        <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
12030          <defs>
12031            <filter id="svgSpinnersGooeyBalls20">
12032              <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
12033              <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"/>
12034              <feBlend in="SourceGraphic" in2="z"/>
12035            </filter>
12036          </defs>
12037          <g filter="url(#svgSpinnersGooeyBalls20)">
12038            <circle cx="5" cy="12" r="4" fill="currentColor">
12039              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
12040            </circle>
12041            <circle cx="19" cy="12" r="4" fill="currentColor">
12042              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
12043            </circle>
12044            <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
12045          </g>
12046        </svg>
12047    `;
12048    
12049    const text = document.createElement('div');
12050    text.textContent = 'Duplicating gallery...';
12051    text.style.textAlign = 'center';
12052    
12053    message.appendChild(spinner);
12054    message.appendChild(text);
12055    overlay.appendChild(message);
12056    document.body.appendChild(overlay);
12057    
12058    return {
12059        hide: function() {
12060            if (overlay && overlay.parentNode) {
12061                overlay.parentNode.removeChild(overlay);
12062            }
12063        },
12064        updateMessage: function(newMessage) {
12065            text.textContent = newMessage;
12066        }
12067    };
12068}
12069
12070/**
12071 * Duplicates a gallery or page item.
12072 * @param {number} id - The ID of the item to duplicate.
12073 * @param {Event} event - The click event.
12074 */
12075async function duplicateGalleryItem(id, event) {
12076    event.stopPropagation();
12077    console.log(`Duplicating item with ID: ${id}`);
12078
12079    // Show loading overlay
12080    const overlay = showDuplicationOverlay();
12081
12082    // Find the original item in the global galleries array
12083    const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find
12084
12085    if (!originalItem) {
12086        console.error(`Cannot duplicate: Item with ID ${id} not found.`);
12087        overlay.hide();
12088        alert('Error: Could not find the item to duplicate.');
12089        return;
12090    }
12091
12092    if (isPageMenuItem(originalItem)) {
12093        overlay.updateMessage('Duplicating page...');
12094    }
12095
12096    try {
12097        // --- Create a Deep Copy ---
12098        // Using JSON.parse/stringify is a common way for simple object structures
12099        // Be cautious if your objects have Dates, functions, Maps, Sets, etc.
12100        const newItem = JSON.parse(JSON.stringify(originalItem));
12101
12102        // --- Modify Copied Item ---
12103        newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID
12104        newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title
12105
12106        // Generate new pageId if it's a page or integrated gallery
12107        if (newItem.isPage || newItem.isIntegrated) {
12108            newItem.pageId = generatePageId(); // Assign a new unique pageId
12109        }
12110
12111        // Generate new slug and URL
12112        const newBaseSlug = slugify(newItem.title); // Use the new title
12113        newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness
12114        newItem.url = `/${newItem.slug}`; // Update URL based on new slug
12115
12116        // Reset children (don't duplicate children for simplicity)
12117        newItem.children = [];
12118
12119        // Deep clone galleryOptions and pageElements, update pageId if needed
12120        if (newItem.galleryOptions) {
12121            newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions));
12122            // Update pageId within options if it exists and matches the old one
12123            if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) {
12124                newItem.galleryOptions.pageId = newItem.pageId;
12125            }
12126            // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original
12127            ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl',
12128             'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp',
12129             'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma']
12130              .forEach(function(field) { delete newItem.galleryOptions[field]; });
12131        }
12132        if (newItem.pageElements) {
12133            newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements));
12134             // Give new IDs to duplicated page elements
12135             newItem.pageElements.forEach(el => {
12136                 el.id = Date.now() + Math.floor(Math.random() * 10000);
12137             });
12138        }
12139
12140        // Reset home page status
12141        newItem.isHomePage = false;
12142
12143        // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) ---
12144        const isDuplicatingPage = isPageMenuItem(originalItem);
12145        console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', {
12146            isDuplicatingPage,
12147            newItemPageId: newItem.pageId,
12148            originalItemPageId: originalItem.pageId
12149        });
12150
12151        if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) {
12152            overlay.updateMessage('Duplicating gallery files...');
12153            console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...');
12154            const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem);
12155            if (!ndjsonDuplicated) {
12156                console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication');
12157                console.error('[Gallery Duplication] User can republish the gallery later if needed');
12158                alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.');
12159                // Don't block the duplication if NDJSON fails - user can republish later
12160            } else {
12161                console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully');
12162            }
12163        } else {
12164            console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', {
12165                isDuplicatingPage,
12166                hasNewPageId: !!newItem.pageId,
12167                hasOriginalPageId: !!originalItem.pageId,
12168                reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' :
12169                    !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId'
12170            });
12171        }
12172
12173        overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...');
12174
12175        // --- Insert into Galleries Array ---
12176        // Find the index of the original item
12177        const originalIndex = galleries.findIndex(item => item.id === id);
12178
12179        if (originalIndex > -1) {
12180            // Insert the new item right after the original
12181            galleries.splice(originalIndex + 1, 0, newItem);
12182            console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`);
12183
12184             // Update positions for all items after the insertion point
12185             for (let i = originalIndex + 1; i < galleries.length; i++) {
12186                 galleries[i].position = i; // Re-assign position based on new index
12187             }
12188
12189        } else {
12190            // Fallback: Add to the end if original couldn't be found (shouldn't happen)
12191            console.warn(`Original item ${id} not found in array, adding duplicate to the end.`);
12192            newItem.position = galleries.length;
12193            galleries.push(newItem);
12194        }
12195
12196        console.log('Rendering galleries immediately after duplication...');
12197        renderGalleries();
12198        // Update active states if necessary (though unlikely needed here)
12199        updateActiveStates();
12200
12201        // --- Save and Refresh ---
12202        console.log('Saving duplicated item...');
12203        saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh
12204            .then(success => {
12205                overlay.hide(); // Hide overlay before page refresh
12206                if (success) {
12207                    console.log('Duplicate saved successfully. Page will refresh.');
12208                    // No need to call renderGalleries() here because saveGalleries will trigger a reload.
12209                } else {
12210                    console.error('Failed to save the duplicated item.');
12211                    alert('Error: Could not save the duplicated item.');
12212                    // Optionally try to revert the galleries array change here if save fails
12213                }
12214            })
12215            .catch(error => {
12216                 overlay.hide(); // Hide overlay on error
12217                 console.error('Error saving duplicated item:', error);
12218                 alert('Error saving duplicated item.');
12219            });
12220
12221    } catch (error) {
12222        overlay.hide(); // Hide overlay on error
12223        console.error('Error duplicating item:', error);
12224        alert('An error occurred while duplicating the item.');
12225    }
12226}
12227
12228/**
12229 * saveGalleries function that always saves FULL data
12230 * Ensures gallery settings are preserved in both localStorage and server
12231 * 
12232 * @param {Object} customData - Optional custom data to save instead of galleries
12233 * @returns {Promise<boolean>} - Success indicator
12234 */
12235
12236function attachLiveSidebarElements(data) {
12237  if (!data || typeof data !== 'object') return data;
12238  var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function');
12239  if (hasGetter) {
12240    var live = window.SidebarManager.getElementsForSave();
12241    if (Array.isArray(live) && live.length > 0) {
12242      data.sidebarElements = live;
12243      return data;
12244    }
12245    delete data.sidebarElements;
12246    return data;
12247  }
12248  if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) {
12249    delete data.sidebarElements;
12250  }
12251  return data;
12252}
12253
12254async function saveGalleries(customData) {
12255  if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') {
12256    alert('You must be logged in as an admin to save changes');
12257    return false;
12258  }
12259
12260  if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) {
12261    console.warn('Save blocked: help sandbox is active');
12262    return false;
12263  }
12264
12265  function _hasHelpDemoInTree(items) {
12266    if (!Array.isArray(items)) return false;
12267    for (var i = 0; i < items.length; i++) {
12268      var g = items[i];
12269      if (!g) continue;
12270      if (g.isHelpDemo || Number(g.id) < 0) return true;
12271      if (g.children && _hasHelpDemoInTree(g.children)) return true;
12272    }
12273    return false;
12274  }
12275
12276  if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) {
12277    console.warn('Save blocked: active help demo gallery');
12278    return false;
12279  }
12280
12281  var savePayload = customData && customData.galleries ? customData.galleries : galleries;
12282  if (_hasHelpDemoInTree(savePayload)) {
12283    console.warn('Save blocked: payload contains help demo records');
12284    return false;
12285  }
12286
12287  // Prevent multiple simultaneous saves — queue the latest payload instead of dropping it
12288  if (window._saveInProgress) {
12289    console.log('Save already in progress, queueing latest payload');
12290    if (customData) {
12291      window._pendingSaveGalleriesData = attachLiveSidebarElements(customData);
12292    }
12293    return new Promise(function(resolve) {
12294      window._pendingSaveResolvers = window._pendingSaveResolvers || [];
12295      window._pendingSaveResolvers.push(resolve);
12296    });
12297  }
12298
12299  window._saveInProgress = true;
12300
12301  try {
12302    // CRITICAL: Synchronize galleries before saving to ensure we use the latest data
12303    if (typeof synchronizeGalleries === 'function') {
12304      synchronizeGalleries();
12305      console.log('Galleries synchronized before saving');
12306    }
12307
12308    // Try to get a valid token from multiple sources
12309    let token = null;
12310    let tokenSource = 'none';
12311    
12312    // First try the global didToken (traditional approach)
12313    if (didToken) {
12314      console.log('Using global didToken for save operation');
12315      token = didToken;
12316      tokenSource = 'didToken';
12317    } 
12318    // Next try TokenManager if available
12319    else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
12320      token = window.TokenManager.getToken();
12321      
12322      // Remove Bearer prefix if present (we'll add it later)
12323      if (token && token.startsWith('Bearer ')) {
12324        token = token.substring(7);
12325      }
12326      
12327      console.log('Using TokenManager token for save operation, length:', token ? token.length : 0);
12328      tokenSource = 'TokenManager';
12329    }
12330    // Fallback to localStorage
12331    else if (localStorage.getItem('hydra_auth_token')) {
12332      token = localStorage.getItem('hydra_auth_token');
12333      console.log('Using localStorage token for save operation, length:', token.length);
12334      tokenSource = 'localStorage';
12335    }
12336    // Last resort: try to get a fresh token
12337    else if (magic && magic.user) {
12338      try {
12339        console.log('Attempting to get fresh token from Magic');
12340        token = await magic.user.getIdToken();
12341        console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed');
12342        tokenSource = token ? 'magic' : 'none';
12343      } catch (e) {
12344        console.error('Error getting token from Magic:', e);
12345        tokenSource = 'magic_error';
12346      }
12347    }
12348    
12349    // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request
12350    const loggedInEmail = (window.userMetadata && window.userMetadata.email) ||
12351      localStorage.getItem('hydra_auth_email') ||
12352      '';
12353    console.log('[AUTH_SAVE] Token resolved:', {
12354      loggedInEmail: loggedInEmail || '(none)',
12355      tokenSource,
12356      tokenLength: token ? token.length : 0,
12357      tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)',
12358      hasToken: !!token,
12359      isAuthenticated,
12360      hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true',
12361      hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'),
12362      hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function')
12363    });
12364    
12365    if (!token) {
12366      console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.');
12367      throw new Error('Could not find a valid authentication token. Please log in again.');
12368    }
12369
12370    // IMPORTANT: Try to save to localStorage with isolated error handling
12371    // This ensures localStorage failures don't prevent server saves
12372    if (!customData) { // Only try localStorage for regular gallery saves
12373      try {
12374        // FIXED: Always save the FULL galleries data to localStorage
12375        localStorage.setItem('galleries', JSON.stringify(galleries));
12376        console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`);
12377      } catch (storageError) {
12378        // Now storage errors are isolated and non-fatal
12379        console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError);
12380      }
12381    } else if (customData && customData.isImport) {
12382      // Skip localStorage for import operations - data too large
12383      console.log('Skipping localStorage for import operation - data too large for local storage');
12384    }
12385
12386    // IMPORTANT: Ensure all pages have proper URLs and slugs before saving
12387    galleries.forEach(gallery => {
12388      if (gallery.isPage === true) {
12389        const pageSlug = slugify(gallery.title);
12390        
12391        // Make sure slug and URL are set correctly
12392        if (!gallery.slug || gallery.slug === '') {
12393          gallery.slug = pageSlug;
12394        }
12395        
12396        // Make sure URL points to slug
12397        if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) {
12398          gallery.url = `/${pageSlug}`;
12399        }
12400      }
12401    });
12402
12403    // Set up the data to save - ensure we're using the complete data
12404    let siteData;
12405    console.log('saveGalleries: customData received:', JSON.stringify(customData));
12406    console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata);
12407    console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title);
12408    console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics);
12409    if (customData) {
12410      // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar
12411      // If customData contains galleries (which it should for sidebar-initiated saves),
12412      // merge them with any existing settings to ensure nothing is lost
12413      if (customData.galleries && Array.isArray(customData.galleries)) {
12414        // First, ensure we preserve all current gallery settings from the existing galleries
12415        const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || [];
12416        
12417        // Create merged galleries array with preserved settings
12418        const mergedGalleries = customData.galleries.map(gallery => {
12419          const existingGallery = existingGalleries.find(g => g.id === gallery.id);
12420          
12421          // If this gallery exists in the current dataset, ensure all galleryOptions are preserved
12422          if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) {
12423            // Calculate settings depth
12424            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12425            const serverOptionsDepth = Object.keys(gallery.galleryOptions).length;
12426            
12427            // If server has fewer settings, merge them with existing settings
12428            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12429              console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12430              
12431              // Create a merged gallery with preserved settings
12432              return {
12433                ...gallery,
12434                galleryOptions: {
12435                  ...existingGallery.galleryOptions, // Start with all existing settings
12436                  ...gallery.galleryOptions    // Override with any new server settings
12437                }
12438              };
12439            }
12440          }
12441          
12442          // If no preservation needed, use server gallery as is
12443          return gallery;
12444        });
12445        
12446        // Use the merged galleries in customData
12447        siteData = {
12448          ...customData,
12449          galleries: mergedGalleries
12450        };
12451        
12452        console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`);
12453        console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData));
12454        console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata);
12455        console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title);
12456        console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics);
12457      } else {
12458        siteData = customData;
12459      }
12460    } else {
12461      const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries))
12462        ? window.galleries
12463        : galleries;
12464      siteData = {
12465        galleries: galleriesForSave
12466      };
12467      
12468      if (window.siteMetadata) {
12469        siteData.siteMetadata = window.siteMetadata;
12470      }
12471
12472      if (window.SidebarManager) {
12473        const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function'
12474          ? window.SidebarManager.getElementsForSave()
12475          : null;
12476        if (Array.isArray(liveSidebar) && liveSidebar.length > 0) {
12477          siteData.sidebarElements = liveSidebar;
12478        }
12479      }
12480    }
12481
12482    attachLiveSidebarElements(siteData);
12483
12484    // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12485    // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.)
12486    if (siteData.galleries && Array.isArray(siteData.galleries)) {
12487      siteData.galleries = siteData.galleries.map(g => {
12488        if (g && g.galleryOptions && typeof g.galleryOptions === 'object') {
12489          return { ...g, isPage: false };
12490        }
12491        return g;
12492      });
12493    }
12494
12495    // Get the save URL
12496    const saveUrl = typeof getApiUrl === 'function' ? 
12497      getApiUrl('/api/save-config') : 
12498      '/api/save-config';
12499    
12500    // Get user email from various sources
12501    const userEmail = (window.userMetadata && window.userMetadata.email) || 
12502                     localStorage.getItem('hydra_auth_email') || 
12503                     '';
12504    
12505    // Ensure token has proper format
12506    const formattedToken = token.startsWith('hydra:') ? token :
12507                          (token.includes('.') ? token : `hydra:${token}`);
12508    
12509    console.log('Making server save request...');
12510    console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData));
12511    console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata);
12512    console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title);
12513    console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics);
12514    
12515    // [AUTH_SAVE] Log for S+J download - request details (no full token)
12516    console.log('[AUTH_SAVE] Sending save request:', {
12517      saveUrl,
12518      authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`,
12519      userEmail: userEmail || '(empty)',
12520      tokenSource,
12521      formattedTokenLength: formattedToken.length
12522    });
12523    
12524    // Make the request
12525    let response = await fetch(saveUrl, {
12526      method: 'POST',
12527      headers: {
12528        'Content-Type': 'application/json',
12529        'Authorization': `Bearer ${formattedToken}`,
12530        'X-User-Email': userEmail,
12531        'X-API-Request': 'true' // Always include this header for API requests
12532      },
12533      body: JSON.stringify(siteData)
12534    });
12535
12536    console.log('Save response status:', response.status);
12537    
12538    // Process the response
12539    let responseText = await response.text();
12540    console.log('Server response text (first 500 chars):', responseText.substring(0, 500));
12541    let responseData;
12542    
12543    try {
12544      responseData = JSON.parse(responseText);
12545        console.log('Server response parsed successfully:', responseData);
12546        if (responseData.debug) {
12547          console.log('Server debug info:', responseData.debug);
12548          console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys);
12549          console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata);
12550          console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata);
12551          console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata);
12552          console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics);
12553        }
12554    } catch (jsonError) {
12555      if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) {
12556        responseData = { success: true };
12557      } else {
12558        throw new Error('Invalid response format: ' + responseText.substring(0, 100));
12559      }
12560    }
12561
12562    if (!response.ok && !responseData.success) {
12563      // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details
12564      const authSaveFailure = {
12565        status: response.status,
12566        statusText: response.statusText,
12567        error: responseData && responseData.error,
12568        details: responseData && responseData.details,
12569        authMethod: responseData && responseData.authMethod,
12570        fullResponse: responseData,
12571        responseTextPreview: responseText.substring(0, 300)
12572      };
12573      console.error('[AUTH_SAVE] Save request failed:', authSaveFailure);
12574      
12575      // Retry once on 401: token may have expired - try refresh or fresh Magic token
12576      if (response.status === 401) {
12577        console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...');
12578        let newToken = null;
12579        if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') {
12580          newToken = await window.TokenManager.refreshToken();
12581        }
12582        if (!newToken && magic && magic.user) {
12583          try {
12584            newToken = await magic.user.getIdToken();
12585          } catch (e) {
12586            console.warn('[AUTH_SAVE] Magic getIdToken failed:', e);
12587          }
12588        }
12589        if (newToken) {
12590          if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7);
12591          token = newToken;
12592          didToken = newToken;
12593          localStorage.setItem('hydra_auth_token', newToken);
12594          if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12595            window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail);
12596          }
12597          console.log('[AUTH_SAVE] Got fresh token, retrying save...');
12598          const retryResponse = await fetch(saveUrl, {
12599            method: 'POST',
12600            headers: {
12601              'Content-Type': 'application/json',
12602              'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`,
12603              'X-User-Email': userEmail,
12604              'X-API-Request': 'true'
12605            },
12606            body: JSON.stringify(siteData)
12607          });
12608          const retryText = await retryResponse.text();
12609          let retryData;
12610          try { retryData = JSON.parse(retryText); } catch (e) {
12611            if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true };
12612            else throw new Error('Invalid retry response');
12613          }
12614          if (retryResponse.ok || retryData.success) {
12615            response = retryResponse;
12616            responseText = retryText;
12617            responseData = retryData;
12618            console.log('[AUTH_SAVE] Retry succeeded');
12619          } else {
12620            console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData);
12621            throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12622          }
12623        } else {
12624          throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12625        }
12626      } else {
12627        throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12628      }
12629    }
12630
12631    console.log('Configuration saved successfully');
12632
12633    // Update local lastKnownConfigTimestamp after successful save
12634    if (responseData.lastModified) {
12635      window.lastKnownConfigTimestamp = responseData.lastModified;
12636      console.error('[CONFLICT DETECT] ✅ Updated local config timestamp from save response:', window.lastKnownConfigTimestamp);
12637    } else {
12638      console.error('[CONFLICT DETECT] ⚠️ No lastModified in save response, fetching from server...');
12639      // If server doesn't return timestamp, fetch it
12640      try {
12641        const timestampUrl = typeof getApiUrl === 'function' ? 
12642          getApiUrl('/api/config-timestamp') : 
12643          '/api/config-timestamp';
12644        const timestampResponse = await fetch(timestampUrl);
12645        if (timestampResponse.ok) {
12646          const timestampData = await timestampResponse.json();
12647          if (timestampData.lastModified) {
12648            window.lastKnownConfigTimestamp = timestampData.lastModified;
12649            console.error('[CONFLICT DETECT] ✅ Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp);
12650          } else {
12651            console.error('[CONFLICT DETECT] ⚠️ No lastModified in fetched timestamp data:', timestampData);
12652          }
12653        } else {
12654          console.error('[CONFLICT DETECT] ⚠️ Failed to fetch timestamp after save, status:', timestampResponse.status);
12655        }
12656      } catch (err) {
12657        console.error('[CONFLICT DETECT] ⚠️ Could not fetch timestamp after save:', err);
12658      }
12659    }
12660
12661    // Store new token if provided
12662    if (responseData.hydraToken) {
12663      console.log('Received new hydra token from server');
12664      
12665      // Update the global didToken
12666      didToken = responseData.hydraToken.startsWith('hydra:') ? 
12667        responseData.hydraToken.substring(6) : responseData.hydraToken;
12668      
12669      // Update localStorage
12670      localStorage.setItem('hydra_auth_token', didToken);
12671      
12672      // Update TokenManager if available
12673      if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12674        window.TokenManager.storeToken(responseData.hydraToken, userEmail);
12675      }
12676    }
12677
12678    // Process server response data if needed
12679    if (responseData.galleries) {
12680      // If server returns updated galleries data, update the global galleries variable
12681      if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) {
12682        // CRITICAL FIX: Before replacing galleries with server response,
12683        // preserve any gallery settings that might be lost in the response
12684        const updatedGalleries = responseData.galleries.map(serverGallery => {
12685          // Find the matching gallery in our current data
12686          const existingGallery = galleries.find(g => g.id === serverGallery.id);
12687          
12688          // Check if we need to preserve gallery settings
12689          if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) {
12690            // Calculate settings depth
12691            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12692            const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length;
12693            
12694            // If server has fewer settings, merge them with existing settings
12695            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12696              console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12697              
12698              // Create a merged gallery with preserved settings
12699              return {
12700                ...serverGallery,
12701                galleryOptions: {
12702                  ...existingGallery.galleryOptions, // Start with all existing settings
12703                  ...serverGallery.galleryOptions    // Override with any new server settings
12704                }
12705              };
12706            }
12707          }
12708          
12709          // If no preservation needed, use server gallery as is
12710          return serverGallery;
12711        });
12712        
12713        // Update the galleries variable with our preserved data
12714        galleries = updatedGalleries;
12715        console.log(`Updated galleries from server response: ${galleries.length} galleries`);
12716        
12717        // IMPORTANT: Synchronize again after receiving server response
12718        if (typeof synchronizeGalleries === 'function') {
12719          synchronizeGalleries();
12720          console.log('Galleries re-synchronized after server response');
12721        }
12722        
12723        // Keep sidebar UI in sync with current galleries (e.g. after delete)
12724        if (typeof renderGalleries === 'function') {
12725          renderGalleries();
12726        }
12727        
12728        // Update localStorage with full data
12729        try {
12730          localStorage.setItem('galleries', JSON.stringify(galleries));
12731          console.log('Updated localStorage with server gallery data (FULL format)');
12732        } catch (e) {
12733          console.warn('Could not update localStorage with server gallery data', e);
12734        }
12735      }
12736    }
12737
12738    // CRITICAL FIX: Clear all sessionStorage caches after successful save
12739    // This ensures that when users open the site in a new tab, they get fresh data
12740    try {
12741      console.log('Clearing sessionStorage caches after successful save...');
12742      const keysToRemove = [];
12743      for (let i = 0; i < sessionStorage.length; i++) {
12744        const key = sessionStorage.key(i);
12745        if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) {
12746          keysToRemove.push(key);
12747        }
12748      }
12749      keysToRemove.forEach(key => {
12750        sessionStorage.removeItem(key);
12751        console.log('Cleared sessionStorage key: ' + key);
12752      });
12753      console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries');
12754    } catch (cacheError) {
12755      console.warn('Error clearing sessionStorage cache:', cacheError);
12756      // Don't fail the save if cache clearing fails
12757    }
12758
12759    window._saveInProgress = false;
12760
12761    if (!customData || !Array.isArray(customData.sidebarElements)) {
12762      document.dispatchEvent(new CustomEvent('sidebar-save-requested'));
12763    }
12764
12765    // Flush any save queued while this request was in flight
12766    if (window._pendingSaveGalleriesData) {
12767      const pending = window._pendingSaveGalleriesData;
12768      window._pendingSaveGalleriesData = null;
12769      const resolvers = window._pendingSaveResolvers || [];
12770      window._pendingSaveResolvers = [];
12771      return saveGalleries(pending).then(function(result) {
12772        resolvers.forEach(function(r) { r(result); });
12773        return result;
12774      });
12775    }
12776
12777    const idleResolvers = window._pendingSaveResolvers || [];
12778    window._pendingSaveResolvers = [];
12779    idleResolvers.forEach(function(r) { r(true); });
12780
12781    return true;
12782  } catch (error) {
12783    // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401)
12784    console.error('[AUTH_SAVE] Save threw exception:', {
12785      message: error.message,
12786      name: error.name,
12787      stack: error.stack
12788    });
12789    console.error('Error saving configuration:', error);
12790    alert('Failed to save changes: ' + error.message);
12791    window._saveInProgress = false;
12792    const failResolvers = window._pendingSaveResolvers || [];
12793    window._pendingSaveResolvers = [];
12794    failResolvers.forEach(function(r) { r(false); });
12795    return false;
12796  }
12797}
12798
12799// Helper function to get API URL for preview sites
12800function getApiUrl(endpoint) {
12801  // Check if we're on a preview URL
12802  const isPreviewUrl = window.location.hostname === 'preview.neonsky.app';
12803  
12804  if (isPreviewUrl) {
12805    // Extract the site GUID from the path (first segment after domain)
12806    const pathParts = window.location.pathname.split('/').filter(Boolean);
12807    if (pathParts.length > 0) {
12808      const siteGuid = pathParts[0];
12809      // Include the site GUID in the API URL
12810      return `/${siteGuid}${endpoint}`;
12811    }
12812  }
12813  
12814  // Regular case - return the endpoint as is
12815  return endpoint;
12816}
12817
12818function updateActiveStates() {
12819  // Get the most current active gallery ID, ensuring consistency between both variables
12820  const currentActiveId = window.activeGalleryId || activeGalleryId;
12821  
12822  // Ensure both variables are synchronized
12823  window.activeGalleryId = currentActiveId;
12824  activeGalleryId = currentActiveId;
12825  
12826  console.log("updateActiveStates: Using active gallery ID:", currentActiveId);
12827  
12828  // Special case for invisible home page - don't try to highlight it in the menu
12829  const activeGallery = galleries.find(g => g.id === currentActiveId);
12830  if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) {
12831    console.log("Active gallery is invisible home page - not highlighting in menu");
12832    // We're not returning here, so it will still update the visibility classes
12833    // but won't try to find an element to mark as active
12834  }
12835  
12836  // Fix selector issue - look for both .tree and .sidebar
12837  document.querySelectorAll('.tree li, .sidebar li').forEach(li => {
12838    if (!li.dataset.id) return; // Skip items without data-id attribute
12839    
12840    const id = parseInt(li.dataset.id);
12841    if (isNaN(id)) return; // Skip if ID is not a number
12842    
12843    const gallery = galleries.find(g => g.id === id);
12844    
12845    // Update active state using the most current ID
12846    const isActive = id === currentActiveId;
12847    
12848    // Add or remove the active class
12849    if (isActive) {
12850      li.classList.add('active');
12851      console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`);
12852    } else {
12853      li.classList.remove('active');
12854    }
12855    
12856    // Update visibility class if gallery exists
12857    if (gallery) {
12858      li.classList.toggle('hidden-gallery', gallery.visible === false);
12859      
12860      // Update visibility toggle icon if it exists
12861      const toggleButton = li.querySelector('.visibility-toggle');
12862      if (toggleButton) {
12863        toggleButton.classList.toggle('hidden', gallery.visible === false);
12864      }
12865    }
12866  });
12867  
12868  // Verify that the active state was actually set for the current activeGalleryId
12869  const activeElements = document.querySelectorAll('.active');
12870  if (activeElements.length === 0 && currentActiveId) {
12871    // Only log a warning if we're not dealing with an invisible home page
12872    const activeGallery = galleries.find(g => g.id === currentActiveId);
12873    if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) {
12874      console.warn(`No active elements found after update for ID: ${currentActiveId}`);
12875    }
12876  }
12877  
12878  // Log active items for debugging
12879  console.log("Active gallery ID after update:", currentActiveId);
12880  activeElements.forEach(el => 
12881    console.log("Active element:", el.textContent.trim())
12882  );
12883}
12884
12885
12886function debugGalleryStructure() {
12887console.group('Gallery Structure Debug');
12888
12889// Log the raw data structure
12890console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries)));
12891
12892// Log the tree structure
12893const treeStructure = createGalleryTree(galleries);
12894
12895// Helper to print the tree
12896function printTree(items, level = 0) {
12897const indent = '  '.repeat(level);
12898items.forEach(item => {
12899  console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`);
12900  if (item.children && item.children.length > 0) {
12901    printTree(item.children, level + 1);
12902  }
12903});
12904}
12905
12906console.log('Tree structure:');
12907printTree(treeStructure);
12908
12909// Log the DOM structure
12910console.log('DOM structure:');
12911const treeEl = document.getElementById('galleryTree');
12912console.log(treeEl);
12913
12914console.groupEnd();
12915}
12916
12917function createGalleryTree(galleries) {
12918console.log("Creating gallery tree with preservation safeguards...");
12919
12920// First, create a map of galleries by id for quick lookup
12921const galleryMap = {};
12922const processedIds = new Set();
12923
12924// First pass: Create clean copies without children
12925galleries.forEach(gallery => {
12926if (!gallery || !gallery.id) return; // Skip invalid galleries
12927
12928// Create a copy of the gallery object without the children field
12929const cleanGallery = { ...gallery };
12930delete cleanGallery.children;
12931
12932// Initialize an empty children array
12933cleanGallery.children = [];
12934
12935// Add to map - even if we've seen this ID before, keep the latest version
12936if (processedIds.has(gallery.id)) {
12937  console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`);
12938}
12939
12940processedIds.add(gallery.id);
12941galleryMap[gallery.id] = cleanGallery;
12942});
12943
12944// Second pass: Assign children to their parents
12945const rootGalleries = [];
12946
12947// To detect already-processed children
12948const childrenAssigned = new Set();
12949
12950galleries.forEach(gallery => {
12951if (!gallery || !gallery.id) return; // Skip invalid galleries
12952
12953const galleryId = gallery.id;
12954
12955// Skip if this gallery isn't in our map (could be invalid)
12956if (!galleryMap[galleryId]) return;
12957
12958// Skip if we've already assigned this gallery as a child
12959if (childrenAssigned.has(galleryId)) return;
12960
12961if (gallery.parentId && galleryMap[gallery.parentId]) {
12962  // This is a child gallery, but first check it doesn't create a circular reference
12963  
12964  // Find all parents in the chain up to the root
12965  let currentParent = galleryMap[gallery.parentId];
12966  let safeToAdd = true;
12967  let chainIds = [galleryId, gallery.parentId];
12968  
12969  while (currentParent.parentId) {
12970    // If we've seen this ID in the chain, it would create a circular reference
12971    if (chainIds.includes(currentParent.parentId)) {
12972      console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`);
12973      safeToAdd = false;
12974      break;
12975    }
12976    
12977    chainIds.push(currentParent.parentId);
12978    currentParent = galleryMap[currentParent.parentId];
12979    
12980    // If parent doesn't exist in the map, break the chain
12981    if (!currentParent) break;
12982  }
12983  
12984  if (safeToAdd) {
12985    // Safe to add as child
12986    galleryMap[gallery.parentId].children.push(galleryMap[galleryId]);
12987    childrenAssigned.add(galleryId);
12988  } else {
12989    // Not safe - make it a root gallery
12990    rootGalleries.push(galleryMap[galleryId]);
12991  }
12992} else {
12993  // This is a root gallery (no parent or parent doesn't exist)
12994  if (!childrenAssigned.has(galleryId)) {
12995    rootGalleries.push(galleryMap[galleryId]);
12996  }
12997}
12998});
12999
13000// Final verification
13001const allGalleryIds = new Set(galleries.map(g => g.id));
13002const treeGalleryIds = new Set();
13003
13004// Function to collect all IDs in the tree
13005const collectIds = (items) => {
13006items.forEach(item => {
13007  treeGalleryIds.add(item.id);
13008  if (item.children && item.children.length > 0) {
13009    collectIds(item.children);
13010  }
13011});
13012};
13013
13014collectIds(rootGalleries);
13015
13016// Check if any galleries are missing from the tree
13017const missingIds = [];
13018allGalleryIds.forEach(id => {
13019if (!treeGalleryIds.has(id)) {
13020  missingIds.push(id);
13021}
13022});
13023
13024if (missingIds.length > 0) {
13025console.warn(`${missingIds.length} galleries are missing from the tree structure`);
13026
13027// Add the missing galleries as root items
13028missingIds.forEach(id => {
13029  const gallery = galleryMap[id];
13030  if (gallery && !childrenAssigned.has(id)) {
13031    console.log(`Adding missing gallery to root: ${gallery.title}`);
13032    rootGalleries.push(gallery);
13033  }
13034});
13035}
13036
13037console.log(`Created tree with ${rootGalleries.length} root galleries`);
13038return rootGalleries;
13039}
13040
13041function alignFolderStates() {
13042  console.log("Aligning folder data states with collapsed visual appearance");
13043  let changed = 0;
13044  
13045  galleries.forEach(gallery => {
13046    if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) {
13047      gallery.isCollapsed = true;
13048      changed++;
13049    }
13050  });
13051  
13052}
13053
13054
13055
13056function renderGalleryItem(gallery, level = 0, maxLevel = 10) {
13057  // Prevent infinite recursion by limiting depth
13058  if (level >= maxLevel) {
13059    console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`);
13060    return '';
13061  }
13062
13063  // Add debug info to console
13064  console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`);
13065
13066  // Skip hidden items in view mode (but not spacers)
13067  if (!isEditing && gallery.visible === false && !gallery.isSpacer) {
13068    return '';
13069  }
13070  
13071const isPage = isPageMenuItem(gallery);
13072const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu
13073const isNavigableParent = isNavigableMenuParent(gallery);
13074const isSpacer = gallery.isSpacer === true;
13075const isExternal = gallery.isExternal === true;
13076  
13077// CRITICAL FIX: Ensure children are always populated for folders
13078// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array
13079// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode
13080if (isFolder) {
13081  // Always ensure children array exists
13082  if (!gallery.children) {
13083    gallery.children = [];
13084  }
13085  // If children array is empty, build it from flat galleries array using parentId
13086  if (gallery.children.length === 0) {
13087    const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id);
13088    if (childrenFromFlat.length > 0) {
13089      gallery.children = childrenFromFlat;
13090      console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`);
13091    }
13092  }
13093}
13094
13095// Parent-capable items need their chevron and empty nested drop zone immediately,
13096// before the first child has been added.
13097const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0;
13098const hasChildrenClass = hasChildren ? 'has-children' : '';
13099
13100// Standard collapsed state check
13101const isCollapsed = gallery.isCollapsed === true;
13102
13103// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class
13104const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : '';
13105
13106// Add additional classes for special types
13107const spacerClass = isSpacer ? 'spacer-item' : '';
13108const externalClass = isExternal ? 'external-link' : '';
13109const folderClass = isFolder ? 'folder-item' : '';
13110const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : '';
13111  
13112        const duplicateIconSvg = `
13113    <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24">
13114      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
13115        <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"/>
13116        <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"/>
13117      </g>
13118    </svg>
13119  `;
13120
13121
13122  
13123  // NEW: Add nesting level as a data attribute for styling
13124  const nestingLevelAttr = `data-nesting-level="${level}"`;
13125
13126  // Accessible href for SPA items (loadPage/loadGallery still preventDefault)
13127  const menuHref = gallery.isHomePage
13128    ? '/'
13129    : (gallery.url && String(gallery.url).startsWith('/')
13130        ? gallery.url
13131        : (gallery.slug ? ('/' + gallery.slug) : '#'));
13132  const childrenListId = 'gallery-children-' + gallery.id;
13133  const folderExpandedAttr = !isCollapsed ? 'true' : 'false';
13134  const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer)
13135    ? ' aria-current="page"'
13136    : '';
13137
13138  // Create the gallery item HTML with enhanced nesting indicators
13139  const html = `
13140<li data-id="${gallery.id}" 
13141    ${nestingLevelAttr}
13142    class="${gallery.id === activeGalleryId ? 'active' : ''} 
13143          ${gallery.visible === false ? 'hidden-gallery' : ''} 
13144          ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
13145  <div class="gallery-item-content nested-sortable-handle">
13146    ${hasChildren ? `
13147      <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;')}">
13148        <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
13149          <polyline points="9 6 15 12 9 18"></polyline>
13150        </svg>
13151      </button>
13152    ` : ''}
13153    
13154    <!-- Menu item with different rendering based on type -->
13155    ${isSpacer ? `
13156      <!-- Spacer with non-breaking space to ensure height -->
13157      <span class="menu-item spacer" aria-hidden="true">&nbsp;</span>
13158    ` : isExternal ? `
13159      <!-- External URL uses an anchor tag with target="_blank" -->
13160      <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}>
13161        ${gallery.title}
13162      </a>
13163    ` : isFolder ? `
13164      <!-- Folder item with toggle functionality (button for keyboard / SR) -->
13165      <button type="button" class="menu-item folder" data-folder="true"
13166            onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}">
13167        ${gallery.title}
13168      </button>
13169    ` : `
13170      <!-- Real link for SR/keyboard; SPA handlers keep click behavior -->
13171      <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}>
13172        ${gallery.title}
13173      </a>
13174    `}
13175    
13176<div class="controls">
13177      ${!isSpacer ? `
13178      <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)">
13179        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13180          ${gallery.visible !== false ? 
13181            '<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>' :
13182            '<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>'
13183          }
13184        </svg>
13185      </button>
13186      
13187      <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}">
13188        <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24">
13189          ${gallery.passwordProtected ? 
13190            '<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>' :
13191            '<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"/>'
13192          }
13193        </svg>
13194      </button>
13195      
13196      <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13197        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13198          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
13199          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
13200        </svg>
13201      </button>
13202      ` : ''}
13203
13204        <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate">
13205         ${duplicateIconSvg}
13206      </button>
13207
13208      
13209      <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)">
13210        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13211          <polyline points="3 6 5 6 21 6"></polyline>
13212          <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>
13213        </svg>
13214      </button>
13215${!isSpacer ? `
13216<!-- Add home toggle button -->
13217<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'}">
13218  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13219    ${gallery.isHomePage ? 
13220      '<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"/>' :
13221      '<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>'
13222    }
13223  </svg>
13224</button>
13225` : ''}
13226    </div>
13227  </div>
13228  <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}">
13229    ${gallery.children && gallery.children.length > 0 ? 
13230      gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''}
13231  </ol>
13232</li>
13233`;
13234
13235  return html;
13236}
13237
13238// Additional helper function to add visual indicators during drag operations
13239function addDragVisualFeedback() {
13240  // Add event listener for drag operations
13241  document.addEventListener('dragover', function(e) {
13242    // Find the closest nested sortable list
13243    const nearestList = e.target.closest('.nested-sortable');
13244    if (!nearestList) return;
13245    
13246    // Remove active-drop-target class from all lists
13247    document.querySelectorAll('.active-drop-target').forEach(el => {
13248      el.classList.remove('active-drop-target');
13249    });
13250    
13251    // Add it to the current list
13252    nearestList.classList.add('active-drop-target');
13253    
13254    // Calculate if this would be a nested position
13255    // This is a simplified calculation - SortableJS has its own logic,
13256    // but this helps provide additional visual feedback
13257    const rect = nearestList.getBoundingClientRect();
13258    const distanceFromLeft = e.clientX - rect.left;
13259    
13260    // If we're close to the left edge of a nested list, add a class
13261    if (distanceFromLeft < 30) {
13262      nearestList.classList.add('potential-parent');
13263    } else {
13264      nearestList.classList.remove('potential-parent');
13265    }
13266  });
13267  
13268  // Remove classes when drag ends
13269  document.addEventListener('dragend', function() {
13270    document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => {
13271      el.classList.remove('active-drop-target', 'potential-parent');
13272    });
13273  });
13274}
13275
13276// Make sure the addDragVisualFeedback function is called when the document is ready
13277document.addEventListener('DOMContentLoaded', function() {
13278  // Initialize the visual feedback helper
13279  addDragVisualFeedback();
13280  
13281  // If we're in edit mode, make sure the nested sortables are initialized
13282  if (document.body.classList.contains('edit-mode-active')) {
13283    setTimeout(function() {
13284      initializeNestedSortables();
13285    }, 300);
13286  }
13287});
13288
13289// Update the toggleEditMode function to ensure proper initialization of sortables
13290const originalToggleEditMode = window.toggleEditMode;
13291window.toggleEditMode = function() {
13292    originalToggleEditMode.apply(this, arguments); // Call original function
13293
13294    stopAutoAdvanceTimer();
13295    
13296    const sidebar = document.querySelector('.sidebar');
13297    const editControls = document.querySelector('.edit-controls');
13298
13299    if (currentMenuLayout === 'horizontal') {
13300        if (document.body.classList.contains('edit-mode-active')) {
13301            // In horizontal layout and edit mode is ON: show sidebar and edit controls
13302            if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container
13303            if(editControls) editControls.style.display = 'flex';
13304             // Re-render the sidebar menu for editing
13305            renderGalleries();
13306        } else {
13307            // In horizontal layout and edit mode is OFF: hide sidebar
13308            if(sidebar) sidebar.style.display = 'none';
13309             // Re-render the horizontal menu for viewing
13310            renderHorizontalMenu();
13311        }
13312    } else {
13313        // For other layouts (sidebar, top), ensure sidebar is visible
13314        if(sidebar) sidebar.style.display = 'block'; // Or 'flex'
13315    }
13316};
13317
13318function verifyGalleryStructure() {
13319console.log("Verifying gallery structure integrity...");
13320
13321// Count number of galleries
13322const galleryCount = galleries.length;
13323console.log(`Total galleries in flat array: ${galleryCount}`);
13324
13325// Create tree and count galleries in tree
13326const tree = createGalleryTree([...galleries]);
13327let treeCount = 0;
13328
13329function countGalleriesInTree(items) {
13330items.forEach(item => {
13331  treeCount++;
13332  if (item.children && item.children.length > 0) {
13333    countGalleriesInTree(item.children);
13334  }
13335});
13336}
13337
13338countGalleriesInTree(tree);
13339console.log(`Total galleries in tree structure: ${treeCount}`);
13340
13341if (treeCount !== galleryCount) {
13342console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`);
13343return false;
13344} else {
13345console.log("Gallery structure is consistent");
13346return true;
13347}
13348}
13349
13350function renderGalleries() {
13351console.log("Rendering galleries...");
13352
13353  // Align with the explicit edit toggle only (not admin login / stale sidebar.editing)
13354  if (document.body.classList.contains('edit-mode-active')) {
13355    isEditing = true;
13356  } else if (typeof window.isEditing === 'boolean') {
13357    isEditing = window.isEditing;
13358  }
13359
13360  var galleriesSource = galleries;
13361  if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') {
13362    var helpList = window.HelpSystem.getRenderGalleries();
13363    if (helpList) galleriesSource = helpList;
13364  }
13365
13366try {
13367// Only fix circular references if absolutely necessary
13368const foundCircular = fixCircularReferences();
13369
13370// Only create a new tree if we had to fix circular references
13371let galleryTree;
13372if (foundCircular) {
13373  console.log("Creating fresh gallery tree after fixing circular references");
13374  galleryTree = createGalleryTree(galleriesSource);
13375} else {
13376  console.log("Using existing structure to create gallery tree");
13377  galleryTree = createGalleryTree(galleriesSource);
13378}
13379
13380// Render the tree - IMPORTANT: Add null check
13381const tree = document.getElementById('galleryTree');
13382if (!tree) {
13383  console.warn('galleryTree element not found, cannot render galleries');
13384  return;  // Exit the function if the element doesn't exist
13385}
13386
13387// Render the tree with maximum depth protection
13388tree.innerHTML = `
13389  <ol class="nested-sortable">
13390    ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')}
13391  </ol>
13392`;
13393
13394// Initialize sortable for all nested sortable elements
13395if (isEditing) {
13396  // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables
13397  // This prevents drag issues when items are added and immediately dragged
13398  requestAnimationFrame(() => {
13399    initializeNestedSortables();
13400  });
13401}
13402
13403console.log("Galleries rendered successfully");
13404
13405// Set up folder toggle handler after rendering (works in both view and edit modes)
13406setupFolderToggleHandler();
13407
13408// Verify structure integrity after rendering
13409verifyGalleryStructure();
13410
13411if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
13412} catch (error) {
13413console.error("Error rendering galleries:", error);
13414}
13415}
13416
13417/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */
13418function syncSeoNavA11y() {
13419  const seoNav = document.querySelector('.hydra-seo-nav');
13420  if (!seoNav) return;
13421  const hasVisibleLinks =
13422    document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13423    document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0;
13424  const hideFromAt = hasVisibleLinks;
13425  seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false');
13426  // aria-hidden alone is not enough — focusable descendants must leave the tab order
13427  seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => {
13428    if (hideFromAt) {
13429      if (!el.hasAttribute('data-seo-prev-tabindex')) {
13430        el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : '');
13431      }
13432      el.setAttribute('tabindex', '-1');
13433    } else {
13434      const prev = el.getAttribute('data-seo-prev-tabindex');
13435      if (prev === null) return;
13436      if (prev === '') el.removeAttribute('tabindex');
13437      else el.setAttribute('tabindex', prev);
13438      el.removeAttribute('data-seo-prev-tabindex');
13439    }
13440  });
13441}
13442
13443function getMenuItemHref(galleryItem) {
13444  if (!galleryItem) return '#';
13445  if (galleryItem.isExternal && galleryItem.url) return galleryItem.url;
13446  if (galleryItem.isHomePage) return '/';
13447  if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url;
13448  if (galleryItem.slug) return '/' + galleryItem.slug;
13449  return '#';
13450}
13451
13452
13453function fixCircularReferences() {
13454console.log("Selectively checking for circular references...");
13455
13456// Only process galleries that need checking:
13457// 1. Imported galleries (they're new and might cause issues)
13458// 2. Galleries with extremely deep nesting
13459// 3. Actual circular references
13460
13461const processed = new Set();
13462
13463// First pass: Identify actual circular references without modifying structure
13464const circularIds = new Set();
13465const suspiciousIds = new Set();
13466
13467galleries.forEach(gallery => {
13468if (!gallery || !gallery.id) return;
13469
13470// Skip galleries we've already processed
13471if (processed.has(gallery.id)) return;
13472
13473// Check for self-reference - this is always wrong
13474if (gallery.parentId === gallery.id) {
13475  console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`);
13476  circularIds.add(gallery.id);
13477  return;
13478}
13479
13480// Skip galleries without parents
13481if (!gallery.parentId) {
13482  processed.add(gallery.id);
13483  return;
13484}
13485
13486// Follow the parent chain to detect circular references
13487const parentChain = [gallery.id];
13488let currentId = gallery.parentId;
13489let loopCount = 0;
13490let foundCircular = false;
13491
13492while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops
13493  loopCount++;
13494  
13495  // If we've seen this ID before, it's a circular reference
13496  if (parentChain.includes(currentId)) {
13497    console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`);
13498    circularIds.add(gallery.id);
13499    foundCircular = true;
13500    break;
13501  }
13502  
13503  parentChain.push(currentId);
13504  
13505  // Move up to the next parent
13506  const parent = galleries.find(g => g.id === currentId);
13507  currentId = parent?.parentId;
13508  
13509  // If we can't find the parent, we can stop
13510  if (!parent) break;
13511}
13512
13513// If we hit our loop limit but didn't find a circular reference,
13514// it's suspicious but not definitely circular
13515if (loopCount >= 20 && !foundCircular) {
13516  console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`);
13517  suspiciousIds.add(gallery.id);
13518}
13519
13520// Mark this gallery and all its ancestors as processed
13521parentChain.forEach(id => processed.add(id));
13522});
13523
13524// Second pass: Only fix the identified circular references
13525if (circularIds.size > 0) {
13526console.log(`Fixing ${circularIds.size} confirmed circular references`);
13527
13528galleries.forEach(gallery => {
13529  if (circularIds.has(gallery.id)) {
13530    console.log(`Breaking circular reference for gallery: ${gallery.title}`);
13531    gallery.parentId = null;
13532  }
13533});
13534} else {
13535console.log("No circular references found");
13536}
13537
13538// For suspicious galleries, don't automatically fix them
13539if (suspiciousIds.size > 0) {
13540console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`);
13541}
13542
13543return circularIds.size > 0; // Return true if we fixed anything
13544}
13545
13546// Deep linking implementation that uses the existing slug property
13547
13548// Function to get the slug for a gallery
13549function getGallerySlug(gallery) {
13550// First try to use the existing slug property
13551if (gallery.slug) {
13552console.log(`Using existing gallery slug: ${gallery.slug}`);
13553return gallery.slug;
13554}
13555
13556// If no slug exists, generate one from the title as fallback
13557if (gallery.title) {
13558const generatedSlug = gallery.title
13559  .toLowerCase()
13560  .replace(/[^ws-]/g, '')
13561  .replace(/s+/g, '-')
13562  .replace(/--+/g, '-')
13563  .trim();
13564  
13565console.log(`Generated slug from title: ${generatedSlug}`);
13566return generatedSlug || 'gallery';
13567}
13568
13569// Default fallback
13570console.warn('Gallery has no slug or title, using default');
13571return 'gallery';
13572}
13573
13574// Function to update the URL when a gallery is loaded
13575// Update the URL when a gallery is loaded
13576function updateURLWithGallerySlug(gallery) {
13577  if (!gallery) return;
13578
13579  // Keep bookmarkable /_manage URL — do not rewrite to / or /slug when loading galleries/pages
13580  const __hydraPath = window.location.pathname;
13581  if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') {
13582    console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.');
13583    return;
13584  }
13585
13586  // Get the slug for this gallery/page
13587  let slug = gallery.slug;
13588
13589  if (!slug && gallery.title) {
13590    slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim();
13591  }
13592
13593  // Check if we're in preview mode (add this new code)
13594  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13595  
13596  // Check if current history state already has this galleryId
13597  const currentState = window.history.state;
13598  const currentPath = window.location.pathname;
13599  const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 
13600                               ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug));
13601  
13602  if (isPreviewMode) {
13603    // Extract GUID from current URL path - first segment after domain
13604    const pathParts = window.location.pathname.split('/').filter(Boolean);
13605    const siteGuid = pathParts[0];
13606    
13607    // Create history entry if we have a valid GUID, and it's not already the current state.
13608    // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case.
13609    if (siteGuid && (gallery.isHomePage || slug)) {
13610      const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug);
13611      const alreadyAtPreviewHome = gallery.isHomePage && (
13612        currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/'
13613      );
13614      // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13615      if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) {
13616        console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id);
13617        
13618        // Update browser URL without reloading, preserving the GUID
13619        window.history.pushState(
13620          { galleryId: gallery.id }, 
13621          gallery.title || 'Gallery', 
13622          targetPath
13623        );
13624        
13625        // Update page title
13626        document.title = formatGalleryDocumentTitle(gallery.title);
13627      } else {
13628        console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)');
13629      }
13630    }
13631    
13632    return; // Exit early, we've handled the preview case
13633  }
13634  
13635  // Regular (non-preview) URL handling
13636  if (slug) {
13637    // If this gallery is the designated home page, keep the URL as '/' (canonical home URL)
13638    const targetPath = gallery.isHomePage ? '/' : '/' + slug;
13639    // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13640    if (currentPath !== targetPath || !isAlreadyCurrentPage) {
13641      console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id);
13642      window.history.pushState(
13643        { galleryId: gallery.id }, 
13644        gallery.title || 'Gallery', 
13645        targetPath
13646      );
13647      document.title = formatGalleryDocumentTitle(gallery.title);
13648    } else {
13649      console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update');
13650    }
13651  }
13652}
13653
13654
13655function loadHomePage() {
13656  // Find a gallery marked as home page (regardless of visibility)
13657  const homePage = galleries.find(gallery => gallery.isHomePage === true);
13658  
13659  if (homePage) {
13660    console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`);
13661    
13662    // Update active gallery ID
13663    activeGalleryId = homePage.id;
13664    
13665    // Add a special flag to indicate we're loading the home page
13666    // This allows us to bypass visibility checks
13667    window._loadingHomePage = true;
13668    
13669    // Determine if this is a page or gallery and load appropriately
13670    if (homePage.isPage) {
13671      loadPage(homePage.id);
13672    } else {
13673      loadGallery(homePage.id);
13674    }
13675    
13676    // Clear the flag after loading
13677    setTimeout(() => {
13678      window._loadingHomePage = false;
13679    }, 100);
13680    
13681    // Update UI state
13682    updateActiveStates();
13683    updateMobileTitle();
13684    if (typeof closeMobileMenu === 'function') closeMobileMenu();
13685    
13686    return true;
13687  }
13688  
13689  console.log('No home page defined');
13690  return false;
13691}
13692
13693// 4. Update the handleURLNavigation function to check for home page
13694// Find the existing handleURLNavigation function and modify as follows:
13695// In index.ts
13696function handleURLNavigation() {
13697  const path = window.location.pathname.substring(1); // Remove leading slash
13698  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13699  let slug = path;
13700  let siteGuid = null;
13701
13702  // /_manage is the Hydra editor entry — treat like site root for slug resolution (not a gallery slug)
13703  if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
13704    console.log('[handleURLNavigation] /_manage detected — using root navigation behavior');
13705    slug = '';
13706  }
13707
13708  if (isPreviewMode && path) {
13709    const pathParts = path.split('/');
13710    if (pathParts.length >= 1) {
13711        siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug
13712    }
13713    if (pathParts.length >= 2) {
13714      slug = pathParts[1]; // Slug is the part after GUID
13715      console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`);
13716    } else {
13717      // Only GUID is present, or path is empty after GUID
13718      slug = ''; // Treat as root/home for the given GUID
13719      console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`);
13720    }
13721  }
13722
13723  if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/')
13724    console.log("No specific slug in path (root or GUID-only preview), checking for home page.");
13725    if (!loadHomePage() && galleries.length > 0) {
13726      const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13727      if (firstVisibleGallery) {
13728        console.log("No home page, loading first visible item:", firstVisibleGallery.title);
13729        if (firstVisibleGallery.isPage) {
13730            loadPage(firstVisibleGallery.id);
13731        } else {
13732            loadGallery(firstVisibleGallery.id);
13733        }
13734      } else {
13735        console.log("No home page and no visible items to load for this path.");
13736      }
13737    }
13738  } else {
13739    console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : ""));
13740    // findGalleryByPath should be able to find the item using the slug,
13741    // handling preview mode internally if necessary or by being passed the correct slug.
13742    const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid);
13743
13744    if (matchingGallery) {
13745      console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')');
13746      console.log('🔍 [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements)));
13747      
13748      if (matchingGallery.isSubmenu) {
13749        console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.');
13750        // Potentially, you might want to find and expand its parent here if the UI supports it.
13751      } else {
13752        // Set activeGalleryId *before* calling loadPage/loadGallery
13753        activeGalleryId = matchingGallery.id;
13754        window.activeGalleryId = matchingGallery.id;
13755
13756        // Determine if this is a page by checking multiple indicators
13757        // Check isPage flag, pageId, or pageElements array
13758        const isPage = matchingGallery.isPage === true || 
13759                       (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) ||
13760                       (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0);
13761        
13762        if (isPage) {
13763          console.log('🔍 [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"');
13764          const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
13765          pageLoader(matchingGallery.id);
13766        } else {
13767          console.log('🔍 [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"');
13768          const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
13769          galleryLoader(matchingGallery.id, null, { forceReload: true });
13770        }
13771      }
13772    } else {
13773      console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`);
13774      if (!loadHomePage()) { // Try loading home page
13775        const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13776        if (firstVisibleGallery) {
13777          console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`);
13778           if (firstVisibleGallery.isPage) {
13779               loadPage(firstVisibleGallery.id);
13780           } else {
13781               loadGallery(firstVisibleGallery.id);
13782           }
13783        } else {
13784            console.log("No matching gallery, no home page, and no visible items to load.");
13785            // Optionally, display a 404 message in the gallery-container
13786            const galleryContainer = document.querySelector('.gallery-container');
13787            if (galleryContainer) {
13788                galleryContainer.innerHTML = '<div>Page not found.</div>';
13789                galleryContainer.style.opacity = '1';
13790            }
13791        }
13792      }
13793    }
13794  }
13795
13796  // Update UI states after initiating load.
13797  // These functions should ideally use the now-set activeGalleryId.
13798  if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree
13799  if (typeof updateMobileTitle === 'function') updateMobileTitle();   // For mobile header
13800
13801  // **** ADDED FOR DEEP LINKING HORIZONTAL MENU ****
13802  // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load)
13803  let currentLayout = 'sidebar'; // Default assumption
13804  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
13805    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
13806  } else {
13807    // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings
13808    if (document.body.classList.contains('menu-layout-horizontal')) {
13809        currentLayout = 'horizontal';
13810    }
13811  }
13812
13813  console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`);
13814
13815  if (currentLayout === 'horizontal') {
13816    if (typeof updateActiveStatesHorizontal === 'function') {
13817      console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation.");
13818      // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call.
13819      // A small delay might be needed, or ensure renderHorizontalMenu has run.
13820      // For now, call it directly. If items aren't there, it won't do anything harmful.
13821      setTimeout(() => { // Add a slight delay to allow menu rendering
13822          updateActiveStatesHorizontal();
13823      }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered.
13824    } else {
13825        console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout.");
13826    }
13827  }
13828}
13829
13830/**
13831* Enhanced loadPage function that properly cleans up gallery content
13832* @param {number} id - The page ID
13833* @param {Event} event - Optional event object
13834*/
13835window.loadPage = function(id, event) {
13836  if (event && event.target && event.target.isConnected === false) {
13837    console.log('🔍 [loadPage] Ignoring navigation from detached element');
13838    return;
13839  }
13840  if (event) {
13841    event.preventDefault();
13842    event.stopPropagation();
13843    const now = Date.now();
13844    const lastCallTime = window._lastPageLoadTime || 0;
13845    window._lastPageLoadTime = now;
13846    if (now - lastCallTime < 100) {
13847      console.log('Ignoring duplicate loadPage call');
13848      return;
13849    }
13850  }
13851  console.error('🔍 [loadPage] Loading page with gallery ID:', id);
13852
13853  stopAutoAdvanceTimer();
13854  if (typeof dismissGalleryPasswordOverlay === 'function') {
13855    dismissGalleryPasswordOverlay();
13856  }
13857
13858  let galleriesData = window.galleries || galleries;
13859  const gallery = findGalleryById(galleriesData, id);
13860
13861  if (!gallery) {
13862    console.warn('No gallery found with ID:', id, 'for page load.');
13863    return;
13864  }
13865
13866  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
13867
13868  window.activeGalleryId = id;
13869  activeGalleryId = id;
13870  window._activeContentType = 'page';
13871  if (window.EnhancedGallerySystem) {
13872    window.EnhancedGallerySystem.currentGalleryId = null;
13873  }
13874
13875  if (typeof window.removeGalleryScriptsWithPause === 'function') {
13876    window.removeGalleryScriptsWithPause();
13877  }
13878
13879  const galleryContainer = document.querySelector('.gallery-container');
13880  if (galleryContainer) {
13881    // Clear the container but ensure it's visible
13882    galleryContainer.innerHTML = '';
13883    galleryContainer.style.display = 'block';
13884    galleryContainer.style.visibility = 'visible';
13885    galleryContainer.style.opacity = '1';
13886    console.log('🔍 [loadPage] Cleared and ensured gallery-container is visible');
13887  } else {
13888    console.error('🔍 [loadPage] gallery-container not found!');
13889  }
13890
13891  if (!gallery.pageId) gallery.pageId = `page_${id}`;
13892  if (!gallery.isPage) gallery.isPage = true;
13893
13894  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13895    window.syncGalleryOptionsButtonVisibility();
13896  }
13897
13898  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
13899  window._pageIdToGalleryId[gallery.pageId] = id;
13900
13901  const seedPageElementsOntoManager = function() {
13902    if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
13903      if (window.PageManager && window.PageManager.elements) {
13904        window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
13905      }
13906    } else if (window.PageManager && window.PageManager.elements &&
13907               window.PageManager.elements[gallery.pageId] &&
13908               window.PageManager.elements[gallery.pageId].length > 0) {
13909      gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
13910    }
13911  };
13912  seedPageElementsOntoManager();
13913
13914  const invokePageManagerLoad = function() {
13915    seedPageElementsOntoManager();
13916    try {
13917      console.error('🔍 [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId);
13918      console.error('🔍 [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0);
13919      console.error('🔍 [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0);
13920      console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId]));
13921      
13922      // Small delay to ensure DOM is ready, especially when navigating back via history
13923      setTimeout(() => {
13924        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13925          console.log('🔍 [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId);
13926          return;
13927        }
13928
13929        // Verify container still exists
13930        const containerCheck = document.querySelector('.gallery-container');
13931        console.error('🔍 [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer);
13932        
13933        // PageManager.loadPage is async - we need to properly handle it
13934        try {
13935          const loadPromise = window.PageManager.loadPage(gallery.pageId);
13936          console.error('🔍 [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function');
13937          
13938          if (loadPromise && typeof loadPromise.then === 'function') {
13939            loadPromise.then(() => {
13940              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13941                console.log('🔍 [loadPage] Ignoring stale PageManager completion for:', gallery.pageId);
13942                return;
13943              }
13944
13945              console.error('🔍 [loadPage] PageManager.loadPage promise resolved');
13946              if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13947                window.syncGalleryOptionsButtonVisibility();
13948              }
13949              // Ensure container visibility after PageManager has initialized
13950              if (galleryContainer) {
13951                const pageContainer = galleryContainer.querySelector('.page-container');
13952                if (pageContainer) {
13953                  pageContainer.style.display = 'block';
13954                  pageContainer.style.visibility = 'visible';
13955                  pageContainer.style.opacity = '1';
13956                  const elementCount = pageContainer.querySelectorAll('.page-element').length;
13957                  console.error('🔍 [loadPage] Page container found and made visible, has', elementCount, 'elements');
13958                  if (elementCount === 0) {
13959                    console.error('🔍 [loadPage] WARNING: Page container exists but has no elements!');
13960                    console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]);
13961                  }
13962                } else {
13963                  console.error('❌ [loadPage] Page container not found after PageManager.loadPage');
13964                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
13965                  console.error('❌ [loadPage] Gallery container children:', galleryContainer.children.length);
13966                }
13967              } else {
13968                console.error('❌ [loadPage] Gallery container is null after PageManager.loadPage');
13969              }
13970            }).catch((error) => {
13971              console.error('❌ [loadPage] Error in PageManager.loadPage promise:', error);
13972              console.error('❌ [loadPage] Error stack:', error.stack);
13973            });
13974          } else {
13975            // If loadPage doesn't return a promise, check after a delay
13976            console.error('🔍 [loadPage] loadPage did not return a promise, checking after delay');
13977            setTimeout(() => {
13978              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13979                console.log('🔍 [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId);
13980                return;
13981              }
13982              if (galleryContainer) {
13983                const pageContainer = galleryContainer.querySelector('.page-container');
13984                if (pageContainer) {
13985                  pageContainer.style.display = 'block';
13986                  pageContainer.style.visibility = 'visible';
13987                  pageContainer.style.opacity = '1';
13988                  console.error('🔍 [loadPage] Ensured page-container is visible (non-promise path)');
13989                } else {
13990                  console.error('❌ [loadPage] Page container not found (non-promise path)');
13991                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
13992                }
13993              }
13994            }, 200);
13995          }
13996        } catch (loadError) {
13997          console.error('❌ [loadPage] Exception calling PageManager.loadPage:', loadError);
13998          console.error('❌ [loadPage] Error stack:', loadError.stack);
13999        }
14000        
14001        const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
14002        if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
14003          window.PageManager.setEditMode(isCurrentlyEditing);
14004        }
14005      }, 50); // Small delay to ensure DOM is ready
14006    } catch (error) {
14007      console.error('❌ [loadPage] Error in loadPage function:', error);
14008      console.error('❌ [loadPage] Error stack:', error.stack);
14009    }
14010  };
14011
14012  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
14013    invokePageManagerLoad();
14014  } else {
14015    console.error('❌ [loadPage] PageManager not found yet — waiting for CDN proxy script');
14016    console.error('❌ [loadPage] window.PageManager:', !!window.PageManager);
14017    console.error('❌ [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A');
14018    whenPageManagerReady(function() {
14019      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
14020        console.log('🔍 [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId);
14021        return;
14022      }
14023      console.log('✅ [loadPage] PageManager became available — retrying page render');
14024      invokePageManagerLoad();
14025    });
14026  }
14027
14028  // Apply menu visibility
14029  const bodyEl = document.body;
14030  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14031      bodyEl.classList.add('menu-hidden-on-page');
14032  } else {
14033      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14034  }
14035  applyMenuOverlayClassForGallery(gallery);
14036
14037  if (typeof updateActiveStates === 'function') updateActiveStates();
14038  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14039  if (typeof closeMobileMenu === 'function') closeMobileMenu();
14040  // Close the gallery options panel when navigating to a different page
14041  if (typeof window.closeOptionsPanel === 'function') {
14042    window.closeOptionsPanel();
14043  }
14044  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
14045  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
14046    window.syncGalleryOptionsButtonVisibility();
14047  }
14048  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
14049    window.dispatchEvent(new CustomEvent('page-loaded', {
14050      detail: { pageId: gallery.pageId, galleryId: id }
14051    }));
14052  }
14053}
14054
14055
14056
14057/**
14058* Ensures all required gallery styles are loaded
14059*/
14060function ensureGalleryStylesLoaded() {
14061window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded;
14062// Use Worker route for gallery CSS (avoids CDN SSL issues)
14063const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14064const requiredStyles = [
14065{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' },
14066{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' }
14067];
14068console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css');
14069
14070requiredStyles.forEach(style => {
14071if (!document.getElementById(style.id)) {
14072  const link = document.createElement('link');
14073  link.id = style.id;
14074  link.rel = 'stylesheet';
14075  link.href = style.href;
14076  if (style.href.includes('cdn.neonsky.app')) {
14077    link.onerror = function() {
14078      console.error('❌ Gallery CSS failed, retrying via proxy:', link.href);
14079      // Set flag if not already set
14080      if (!window.useCdnProxy) {
14081        window.useCdnProxy = true;
14082        console.error('🚩 CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests');
14083      }
14084      const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14085      link.href = proxyUrl;
14086      link.onerror = function() {
14087        console.error('❌ Proxy failed, trying storage:', proxyUrl);
14088        link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
14089      };
14090    };
14091  }
14092  document.head.appendChild(link);
14093  console.log(`Added gallery style: ${style.id}`);
14094}
14095});
14096}
14097
14098
14099/**
14100* Ensures all required gallery scripts are loaded
14101* @returns {Promise} A promise that resolves when all scripts are loaded
14102*/
14103function ensureGalleryScriptsLoaded() {
14104const requiredScripts = [
14105{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' },
14106{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' },
14107          { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' }
14108];
14109
14110const promises = requiredScripts.map(script => {
14111return new Promise((resolve, reject) => {
14112  // If script is already loaded, resolve immediately
14113  if (document.getElementById(script.id)) {
14114    resolve();
14115    return;
14116  }
14117  
14118  // Create and load the script
14119  const scriptElement = document.createElement('script');
14120  scriptElement.id = script.id;
14121  scriptElement.src = script.src;
14122  scriptElement.onload = resolve;
14123  scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`));
14124  document.head.appendChild(scriptElement);
14125});
14126});
14127
14128return Promise.all(promises);
14129}
14130
14131/**
14132 * Creates a clean gallery context without using an iframe
14133 * @param {Object} gallery - The gallery object
14134 * @param {HTMLElement} container - The container to load the gallery into
14135 */
14136function createGalleryContext(gallery, container) {
14137  console.log('Creating gallery context for:', gallery.title);
14138
14139  // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script
14140  // This ensures styles are available even if NDJSON fails and falls back to classic JSON
14141  ensureGalleryStylesLoaded();
14142
14143  // Generate the page alias safely
14144  const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-');
14145
14146  // Get our gallery options
14147  const galleryOptions = gallery.galleryOptions || {};
14148
14149  // Get token data
14150  let authToken = null;
14151  // First try TokenManager if available
14152  if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
14153    authToken = window.TokenManager.getToken();
14154  } 
14155  // Then try global token
14156  else if (window.didToken) {
14157    authToken = window.didToken;
14158  }
14159
14160  // Set up Parameters for the gallery
14161  const galleryParameters = {
14162    SiteAlias: window.location.hostname,
14163    InitialPageUuid: gallery.pageId,
14164    InitialPageAlias: pageAlias,
14165    isInEditor: window.isEditing || false,
14166    siteId: window.siteId || '',
14167    isHydra: true
14168  };
14169
14170  // If we have a token, add it
14171  if (authToken) {
14172    galleryParameters.hydraAuthToken = authToken;
14173  }
14174
14175  // Preserve original window.Parameters
14176  const originalParameters = window.Parameters;
14177
14178  // REMOVED: Loading indicator - no longer showing the loading animation
14179
14180  // Load the main gallery script - Use Worker route (avoids CDN SSL issues)
14181  const galleryScript = document.createElement('script');
14182  const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14183  galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js';
14184  console.log('Loading gallery-main script from Worker:', galleryScript.src);
14185  galleryScript.onerror = function() {
14186    console.error('❌ Gallery script failed, retrying via proxy:', galleryScript.src);
14187    const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14188    galleryScript.src = proxyUrl;
14189    galleryScript.onerror = function() {
14190      console.error('❌ Proxy failed, trying storage:', proxyUrl);
14191      galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14192    };
14193  };
14194
14195  // Setup the gallery configuration
14196  const neonGalleryConfig = {
14197    useData: true,
14198    useCDN: true,
14199    version: 'live',
14200    manualCollectionName: "mod",
14201    layoutType: "grid",
14202    ...galleryOptions,
14203    siteId: window.siteId || ''
14204  };
14205
14206  // Execute in a safe way that minimizes global namespace pollution
14207  try {
14208    // Set up the global variables needed by the gallery script
14209    window.Parameters = galleryParameters;
14210    window.neonGalleryConfig = neonGalleryConfig;
14211
14212    // Monitor for script load/error
14213    galleryScript.onload = () => {
14214      console.log('Gallery script loaded successfully for:', gallery.title);
14215      
14216      // REMOVED: Don't need to remove the loading indicator since we don't add it
14217    };
14218
14219    galleryScript.onerror = () => {
14220      console.error('Failed to load gallery script for:', gallery.title);
14221      //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>';
14222      // Restore original parameters
14223      window.Parameters = originalParameters;
14224    };
14225
14226    // Add the script to load the gallery
14227    document.body.appendChild(galleryScript);
14228
14229    // Set up a cleanup function that will be called when another gallery is loaded
14230    container.cleanup = () => {
14231      // Restore original parameters when cleaning up
14232      window.Parameters = originalParameters;
14233      
14234      // Remove the gallery script to prevent conflicts
14235      galleryScript.remove();
14236      
14237      // Clear main gallery container
14238      if (container.parentNode) {
14239        container.innerHTML = '';
14240      }
14241    };
14242  } catch (error) {
14243    console.error('Error creating gallery context:', error);
14244    //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>';
14245    // Restore original parameters
14246    window.Parameters = originalParameters;
14247  }
14248}
14249
14250/**
14251* Only used for the PageManager's initializeDOM - ensures clean content swap
14252*/
14253function cleanGalleryContainer() {
14254const galleryContainer = document.querySelector('.gallery-container');
14255if (galleryContainer) {
14256galleryContainer.innerHTML = '';
14257}
14258}
14259
14260
14261
14262/**
14263* Initializes the page using the PageManager
14264* To be used by PageManager.initializeDOM
14265*/
14266function initPageContent(pageId) {
14267// First clear the container completely
14268cleanGalleryContainer();
14269
14270// Then load the page content
14271if (window.PageManager && window.PageManager.loadPage) {
14272window.PageManager.loadPage(pageId);
14273}
14274}
14275
14276
14277/**
14278* Cleanup gallery content when changing between galleries or pages
14279*/
14280function cleanupGalleryContent() {
14281// Get the gallery container
14282const galleryContainer = document.querySelector('.gallery-container');
14283if (!galleryContainer) return;
14284
14285// Find the direct gallery content
14286const directContent = galleryContainer.querySelector('.gallery-direct-content');
14287if (directContent && typeof directContent.cleanup === 'function') {
14288// Call the cleanup function
14289directContent.cleanup();
14290}
14291
14292// Clear the container
14293galleryContainer.innerHTML = '';
14294}
14295
14296
14297
14298
14299/**
14300* Simplified loadGallery function that follows the same content swap pattern
14301* @param {number} id - The gallery ID
14302* @param {Event} event - Optional event object
14303*/
14304
14305// Override loadGallery to update URL
14306window.loadGallery = async function(id, event) {
14307
14308
14309// Stop event propagation if provided
14310if (event) {
14311  event.stopPropagation();
14312}
14313
14314const galleryContainer = document.querySelector('.gallery-container');
14315if (galleryContainer) {
14316  // Apply immediate hiding styles
14317  galleryContainer.style.opacity = '0';
14318}
14319
14320console.log('Loading gallery with ID:', id);
14321
14322stopAutoAdvanceTimer();
14323if (typeof dismissGalleryPasswordOverlay === 'function') {
14324  dismissGalleryPasswordOverlay();
14325}
14326
14327// Find gallery by ID
14328const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
14329
14330if (!gallery) {
14331  console.warn('No gallery found with ID:', id);
14332  if (galleryContainer) {
14333  galleryContainer.style.opacity = '1';
14334  } // Show container if gallery not found
14335  return;
14336}
14337
14338// Check if this is a page, if so use loadPage instead
14339if (isPageMenuItem(gallery)) {
14340  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
14341  return loadPage(id, event);
14342}
14343
14344// Skip submenu items
14345if (gallery.isSubmenu) {
14346  console.log('Gallery is submenu, skipping URL update');
14347  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
14348  if (galleryContainer) {
14349  galleryContainer.style.opacity = '1';
14350  } // Show container
14351  return;
14352}
14353
14354console.log('Found gallery:', gallery.title);
14355
14356window._activeContentType = 'gallery';
14357beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
14358if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14359  window.PageManager.clearCurrentPage();
14360}
14361
14362// CRITICAL: Set active gallery ID consistently in both global and window scope
14363console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
14364window.activeGalleryId = id;
14365activeGalleryId = id; // Ensure both variables are synchronized
14366
14367// Save reference to which gallery we're loading (for later verification)
14368window.currentLoadingGalleryId = id;
14369
14370// CRITICAL: Thorough cleanup with await to ensure completion
14371if (typeof window.removeGalleryScriptsWithPause === 'function') {
14372  await window.removeGalleryScriptsWithPause();
14373}
14374
14375
14376// Clear all existing content from gallery container
14377if (!galleryContainer) {
14378  console.error('Gallery container not found');
14379  return;
14380}
14381
14382// Force clear all content
14383galleryContainer.innerHTML = '';
14384
14385
14386// CRITICAL: Double-check active gallery ID before updating UI
14387if (window.activeGalleryId !== id || activeGalleryId !== id) {
14388  console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
14389  window.activeGalleryId = id;
14390  activeGalleryId = id;
14391}
14392
14393// Apply menu visibility based on the gallery's setting and edit mode
14394const bodyEl = document.body;
14395if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14396    bodyEl.classList.add('menu-hidden-on-page');
14397} else {
14398    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14399}
14400applyMenuOverlayClassForGallery(gallery);
14401
14402// Update UI state using explicit window function references or local fallbacks
14403if (typeof window.updateActiveStates === 'function') {
14404  window.updateActiveStates();
14405} else if (typeof updateActiveStates === 'function') {
14406  updateActiveStates();
14407}
14408
14409if (typeof window.updateMobileTitle === 'function') {
14410  window.updateMobileTitle();
14411} else if (typeof updateMobileTitle === 'function') {
14412  updateMobileTitle();
14413}
14414
14415if (typeof window.closeMobileMenu === 'function') {
14416  window.closeMobileMenu();
14417} else if (typeof closeMobileMenu === 'function') {
14418  closeMobileMenu();
14419}
14420
14421// Update URL with gallery slug
14422if (typeof window.updateURLWithGallerySlug === 'function') {
14423  window.updateURLWithGallerySlug(gallery);
14424} else if (typeof updateURLWithGallerySlug === 'function') {
14425  updateURLWithGallerySlug(gallery);
14426}
14427
14428  // Create gallery container and load content
14429const neonGalleryContainer = document.createElement('div');
14430neonGalleryContainer.id = 'neon-gallery-container';
14431neonGalleryContainer.className = 'gallery-direct-content';
14432neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
14433neonGalleryContainer.style.width = '100%';
14434neonGalleryContainer.style.height = '100%';
14435galleryContainer.appendChild(neonGalleryContainer);
14436
14437// Add a cache-busting parameter to ensure script is freshly loaded
14438const timestamp = Date.now();
14439
14440// Set up gallery parameters
14441window.Parameters = {
14442  SiteAlias: window.location.hostname,
14443  InitialPageUuid: gallery.pageId,
14444  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
14445  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
14446  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14447  isHydra: true,
14448  galleryInstanceId: timestamp,
14449  // Added loadedGalleryId as per your original function
14450  loadedGalleryId: gallery.id
14451};
14452
14453// Debug: Check what galleryOptions contains
14454console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
14455
14456// Set up gallery config
14457window.neonGalleryConfig = {
14458  useData: true,
14459  useCDN: true,
14460  version: 'live',
14461  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
14462  layoutType: gallery.galleryOptions?.layoutType || "grid",
14463  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
14464  // This ensures that only properties from the current gallery are included, not from previous galleries
14465  ...(gallery.galleryOptions || {}),
14466  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
14467  // These fields are required for the gallery to determine if it should use Load Network racing
14468  loadTxId: gallery.galleryOptions?.loadTxId || null,
14469  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
14470  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
14471  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
14472  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
14473  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
14474  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
14475  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
14476  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14477  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
14478  galleryId: gallery.id,
14479  galleryInstanceId: timestamp
14480};
14481
14482console.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);
14483
14484// Create and add the gallery script with cache-busting
14485// Use Worker route (avoids CDN SSL issues)
14486const script = document.createElement('script');
14487const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14488script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
14489script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
14490script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
14491console.log('Loading gallery-main script from Worker:', script.src);
14492script.onerror = function() {
14493  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
14494  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14495  script.src = proxyUrl;
14496  script.onerror = function() {
14497    console.error('❌ Proxy failed, trying storage:', proxyUrl);
14498    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14499  };
14500};
14501
14502// Added script.onload from your original function
14503script.onload = function() {
14504  if (window.currentLoadingGalleryId !== gallery.id) {
14505    console.warn('Gallery ID mismatch! Expected:', gallery.id,
14506                 'Current:', window.currentLoadingGalleryId);
14507  }
14508  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
14509};
14510
14511
14512if (galleryContainer) {
14513    setTimeout(() => {
14514      galleryContainer.style.opacity = '1';
14515    }, 50); // Small delay to ensure content is ready
14516  }
14517
14518console.log(`Loading fresh gallery script for: ${gallery.title}`);
14519document.body.appendChild(script);
14520}
14521
14522// Function to ensure gallery container is visible
14523function ensureGalleryVisibility() {
14524  const galleryContainer = document.querySelector('.gallery-container');
14525  if (galleryContainer && galleryContainer.style.opacity === '0') {
14526    console.log('ensureGalleryVisibility: Setting gallery container opacity to 1');
14527    galleryContainer.style.opacity = '1';
14528  }
14529}
14530
14531// Handle browser back/forward navigation
14532window.addEventListener('popstate', async function(event) { // Make it async
14533  if (window.__hydraPopstateLock) {
14534    return;
14535  }
14536  window.__hydraPopstateLock = true;
14537  try {
14538  console.error('🔍 [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname);
14539  let galleryIdToLoad = null;
14540  let galleryToLoad = null;
14541
14542  // First, try to get galleryId from event.state
14543  if (event.state && event.state.galleryId) {
14544    galleryIdToLoad = event.state.galleryId;
14545    console.error('🔍 [Popstate] Found galleryId in event.state:', galleryIdToLoad);
14546    // Ensure galleries array is available and use findGalleryById helper
14547    const currentGalleries = window.galleries || galleries || [];
14548    galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad);
14549  }
14550
14551  if (window.__closingNestedLightbox) {
14552    window.__closingNestedLightbox = false;
14553    if (typeof updateActiveStates === 'function') updateActiveStates();
14554    if (typeof updateMobileTitle === 'function') updateMobileTitle();
14555    return;
14556  }
14557
14558  if (window.lightboxNavigationStack?.length > 0 &&
14559      typeof window.closeNestedGalleryLightbox === 'function') {
14560    const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1];
14561    const parentId = stackTop?.parentGalleryId;
14562    if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) {
14563      window.closeNestedGalleryLightbox({ fromPopstate: true });
14564      if (typeof updateActiveStates === 'function') updateActiveStates();
14565      if (typeof updateMobileTitle === 'function') updateMobileTitle();
14566      return;
14567    }
14568  }
14569
14570  if (typeof dismissGalleryPasswordOverlay === 'function') {
14571    dismissGalleryPasswordOverlay();
14572  }
14573
14574  // If no gallery found from state, resolve from URL slug (handles history entries with null state)
14575  if (!galleryToLoad) {
14576    console.error('🔍 [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname);
14577    const path = window.location.pathname.substring(1);
14578    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
14579    let slug = path;
14580    let siteGuid = null;
14581    if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
14582      slug = '';
14583    }
14584    if (isPreviewMode && path) {
14585      const pathParts = path.split('/');
14586      if (pathParts.length >= 1) siteGuid = pathParts[0];
14587      if (pathParts.length >= 2) slug = pathParts[1];
14588      else slug = '';
14589    }
14590    if (slug && typeof findGalleryByPath === 'function') {
14591      galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid);
14592      if (galleryToLoad) {
14593        console.error('🔍 [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14594      }
14595    }
14596  }
14597
14598  // If we have a gallery from state or URL, load it
14599  let contentLoaded = false;
14600  if (galleryToLoad) {
14601    console.error('🔍 [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14602    console.error('🔍 [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements)));
14603    
14604    activeGalleryId = galleryToLoad.id;
14605    window.activeGalleryId = galleryToLoad.id;
14606
14607    // Browser history navigation is explicit user intent — load content regardless of menu visibility.
14608    // Determine if this is a page by checking multiple indicators
14609    const isPage = galleryToLoad.isPage === true || 
14610                   (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) ||
14611                   (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0);
14612    
14613    if (isPage) {
14614      console.error('🔍 [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"');
14615      const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
14616      if (typeof pageLoader === 'function') {
14617          console.error('🔍 [Popstate] Calling loadPage for ID:', galleryToLoad.id);
14618          pageLoader(galleryToLoad.id);
14619          contentLoaded = true;
14620      } else {
14621          console.error('❌ [Popstate] loadPage function not available');
14622      }
14623    } else {
14624      console.error('🔍 [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"');
14625      // Clear page state left over from prior page navigation
14626      if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14627        window.PageManager.clearCurrentPage();
14628      }
14629      window._activeContentType = 'gallery';
14630      const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
14631      if (typeof galleryLoader === 'function') {
14632          await galleryLoader(galleryToLoad.id, null, { forceReload: true });
14633          contentLoaded = true;
14634      } else {
14635          console.error('❌ [Popstate] loadGallery function not available');
14636      }
14637    }
14638  } else {
14639    console.error('❌ [Popstate] No gallery found and handleURLNavigation not available');
14640  }
14641
14642  // Update UI states after loading (or attempting to load) content
14643  if (typeof updateActiveStates === 'function') updateActiveStates();
14644  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14645  
14646  // Ensure gallery visibility after popstate navigation
14647  setTimeout(ensureGalleryVisibility, 1000);
14648
14649  let currentLayout = 'sidebar';
14650  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
14651    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
14652  } else if (document.body.classList.contains('menu-layout-horizontal')) {
14653    currentLayout = 'horizontal';
14654  }
14655
14656  if (currentLayout === 'horizontal') {
14657    if (typeof updateActiveStatesHorizontal === 'function') {
14658      console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay.");
14659      setTimeout(() => {
14660          console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal.");
14661          updateActiveStatesHorizontal();
14662      }, 150); // Slightly longer delay for popstate, as full content might be re-initializing
14663    }
14664  }
14665  } finally {
14666    setTimeout(function() { window.__hydraPopstateLock = false; }, 300);
14667  }
14668});
14669
14670// Check for direct URL access on page load (fallback when bootstrap did not route)
14671document.addEventListener('DOMContentLoaded', function() {
14672setTimeout(function() {
14673console.log('Checking for deep links after DOM content loaded');
14674var __p = window.location.pathname;
14675if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14676  handleURLNavigation();
14677}
14678// Ensure gallery visibility after URL navigation
14679setTimeout(ensureGalleryVisibility, 1000);
14680}, 500);
14681});
14682
14683document.addEventListener('page-save-requested', function(e) {
14684const data = e.detail;
14685if (data && data.pageId && data.elements) {
14686// Find the gallery/page that corresponds to this pageId
14687const page = galleries.find(g => g.pageId === data.pageId);
14688if (page) {
14689  // Store the page elements
14690  page.pageElements = data.elements;
14691  
14692  // Save to server
14693  saveGalleries();
14694}
14695}
14696});
14697
14698document.addEventListener('DOMContentLoaded', function() {
14699function preloadRichTextEditor() {
14700  if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') {
14701    console.log('Preloading Rich Text Editor dependencies');
14702    return window.RichTextEditor._ensureEditorDependencies()
14703      .then(() => console.log('Rich Text Editor dependencies preloaded successfully'))
14704      .catch(error => console.warn('Failed to preload Rich Text Editor:', error));
14705  }
14706
14707  if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') {
14708    return window.PageManager._ensureRichTextEditor()
14709      .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error));
14710  }
14711
14712  return null;
14713}
14714
14715preloadRichTextEditor();
14716setTimeout(preloadRichTextEditor, 1000);
14717setTimeout(preloadRichTextEditor, 3000);
14718});
14719
14720document.addEventListener('DOMContentLoaded', function() {
14721const submenuCheck = document.getElementById('createSubmenu');
14722const submenuTitleField = document.getElementById('submenuTitle');
14723const submenuTitleGroup = submenuTitleField?.parentElement;
14724
14725if (submenuCheck && submenuTitleGroup) {
14726submenuCheck.addEventListener('change', function() {
14727  submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
14728});
14729}
14730});
14731
14732// Also check immediately if document is already loaded (fallback only — avoid duplicate loadPage vs bootstrap)
14733if (document.readyState !== 'loading') {
14734setTimeout(function() {
14735console.log('Document already loaded, checking for deep links');
14736var __p2 = window.location.pathname;
14737if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14738  handleURLNavigation();
14739}
14740}, 300);
14741}
14742
14743// ============================================
14744// CONCURRENT EDIT CONFLICT DETECTION
14745// ============================================
14746(function() {
14747  // Initialize local timestamp tracking
14748  window.lastKnownConfigTimestamp = null;
14749  let conflictPollInterval = null;
14750  
14751  // Initialize timestamp on page load
14752  async function initializeConfigTimestamp() {
14753    try {
14754      const timestampUrl = typeof getApiUrl === 'function' ? 
14755        getApiUrl('/api/config-timestamp') : 
14756        '/api/config-timestamp';
14757      console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl);
14758      const response = await fetch(timestampUrl);
14759      if (response.ok) {
14760        const data = await response.json();
14761        if (data.lastModified) {
14762          window.lastKnownConfigTimestamp = data.lastModified;
14763          console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp);
14764        } else {
14765          console.error('[CONFLICT DETECT] No lastModified in response:', data);
14766        }
14767      } else {
14768        console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status);
14769      }
14770    } catch (error) {
14771      console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error);
14772    }
14773  }
14774  
14775  // Check for conflicts by polling server
14776  async function checkForConflicts() {
14777    // Only poll when in edit mode
14778    if (!document.body.classList.contains('edit-mode-active')) {
14779      console.error('[CONFLICT DETECT] Skipping poll - not in edit mode');
14780      return;
14781    }
14782    
14783    try {
14784      const timestampUrl = typeof getApiUrl === 'function' ? 
14785        getApiUrl('/api/config-timestamp') : 
14786        '/api/config-timestamp';
14787      console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl);
14788      console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp);
14789      
14790      const response = await fetch(timestampUrl);
14791      
14792      if (response.ok) {
14793        const data = await response.json();
14794        const serverTimestamp = data.lastModified;
14795        console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp);
14796        
14797        // If we have a local timestamp and it differs from server, show conflict
14798        if (window.lastKnownConfigTimestamp !== null && 
14799            serverTimestamp !== null && 
14800            serverTimestamp !== window.lastKnownConfigTimestamp) {
14801          console.error('[CONFLICT DETECT] ⚠️ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp);
14802          showConflictIndicator();
14803        } else {
14804          console.error('[CONFLICT DETECT] No conflict - timestamps match or null');
14805          hideConflictIndicator();
14806        }
14807      } else {
14808        console.error('[CONFLICT DETECT] Poll failed with status:', response.status);
14809      }
14810    } catch (error) {
14811      console.error('[CONFLICT DETECT] Error checking for conflicts:', error);
14812    }
14813  }
14814  
14815  // Show conflict indicator icon
14816  function showConflictIndicator() {
14817    const indicator = document.getElementById('conflictIndicator');
14818    console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator);
14819    if (indicator) {
14820      indicator.style.display = 'flex';
14821      indicator.style.visibility = 'visible';
14822      indicator.style.opacity = '1';
14823      indicator.style.background = '#d32f2f';
14824      indicator.style.borderColor = '#d32f2f';
14825      console.error('[CONFLICT DETECT] Conflict indicator displayed');
14826    } else {
14827      console.error('[CONFLICT DETECT] ⚠️ Conflict indicator element not found in DOM!');
14828    }
14829  }
14830  
14831  // Hide conflict indicator icon
14832  function hideConflictIndicator() {
14833    const indicator = document.getElementById('conflictIndicator');
14834    if (indicator) {
14835      indicator.style.display = 'none';
14836      indicator.style.visibility = 'hidden';
14837      indicator.style.opacity = '0';
14838    }
14839  }
14840  
14841  // Start/stop polling based on edit mode
14842  function updatePolling() {
14843    const isEditMode = document.body.classList.contains('edit-mode-active');
14844    
14845    // Always hide indicator when not in edit mode
14846    if (!isEditMode) {
14847      hideConflictIndicator();
14848    }
14849    
14850    if (isEditMode && !conflictPollInterval) {
14851      // Start polling every 10 seconds
14852      conflictPollInterval = setInterval(checkForConflicts, 10000);
14853      console.error('[CONFLICT DETECT] ✅ Started conflict detection polling (every 10s)');
14854      
14855      // Also check immediately
14856      checkForConflicts();
14857    } else if (!isEditMode && conflictPollInterval) {
14858      // Stop polling when not in edit mode
14859      clearInterval(conflictPollInterval);
14860      conflictPollInterval = null;
14861      hideConflictIndicator();
14862      console.error('[CONFLICT DETECT] Stopped conflict detection polling');
14863    }
14864  }
14865  
14866  // Initialize on page load
14867  if (document.readyState === 'loading') {
14868    document.addEventListener('DOMContentLoaded', function() {
14869      initializeConfigTimestamp();
14870      // Watch for edit mode changes
14871      const editModeObserver = new MutationObserver(updatePolling);
14872      editModeObserver.observe(document.body, { 
14873        attributes: true, 
14874        attributeFilter: ['class'] 
14875      });
14876      updatePolling();
14877    });
14878  } else {
14879    initializeConfigTimestamp();
14880    // Watch for edit mode changes
14881    const editModeObserver = new MutationObserver(updatePolling);
14882    editModeObserver.observe(document.body, { 
14883      attributes: true, 
14884      attributeFilter: ['class'] 
14885    });
14886    updatePolling();
14887  }
14888})();
14889
14890</script>
14890
14891</head>
14892<body class="menu-layout-sidebar">
14893  <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a>
14894<nav class="hydra-seo-nav" aria-label="Site navigation">
14895  <ul>
14896    <li><a href="/in-the-same-space">In the Same Space</a></li>
14897    <li><a href="/book">Book</a></li>
14898    <li><a href="/selected-images">Selected images</a></li>
14899    <li><a href="/news">News</a></li>
14900    <li><a href="/latino-muslims">Latino Muslims</a></li>
14901    <li><a href="/refugee-camps-crisis-greece-nytimes">Refugee Camps Crisis Greece-NYTimes</a></li>
14902    <li><a href="/refugee-crisis-greece-wall-street-journal">Refugee Crisis Greece-Wall Street Journal</a></li>
14903    <li><a href="/soul-train-nytimes">Soul Train-NYTimes</a></li>
14904    <li><a href="/philly-fight-night-nytimes">Philly Fight Night-NYTimes</a></li>
14905    <li><a href="/the-decline-of-perama-shipping-yards--the-financial-times">The Decline of Perama Shipping Yards- The Financial Times</a></li>
14906    <li><a href="/centenarians-in-ikaria-the-guardian">Centenarians in Ikaria-The Guardian</a></li>
14907    <li><a href="/syrian-refugees-in-bulgaria-the-new-statesman">Syrian Refugees in Bulgaria-The New Statesman</a></li>
14908    <li><a href="/the-mermaid-madonna">The Mermaid Madonna</a></li>
14909    <li><a href="/party-like-there-is-no-tomorrow">Party Like There is No Tomorrow</a></li>
14910    <li><a href="/selected-tearsheets">Selected Tearsheets</a></li>
14911    <li><a href="/biography-awards">Biography-Awards</a></li>
14912    <li><a href="/nytimes-lens-blog-articles">NYTimes Lens Blog Articles</a></li>
14913    <li><a href="/contact-and-current-location">Contact and current location</a></li>
14914  </ul>
14915</nav>
14916<noscript class="hydra-gallery-noscript">
14917  <article>
14918    <h1>Book | Eirini Vourloumis</h1>
14919    <div class="gallery-overview"><p style="line-height: 0.5; margin-bottom: 0.5em;"><span style="font-size: 14px;"><b>In Waiting</b></span></p><p style="line-height: 0.5;">
14920</p><p style="margin-bottom: 0.5em; line-height: 0.5;"><strong style="font-size: 14px;">Published by Hatje Cantz</strong></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><strong style="font-size: 14px;">Release Date,October 2017</strong></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis</span></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">100 pg, 42 ills.</span></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">hardcover</span></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">9.00 x 24.00 cm</span></p><p style="line-height: 0.5; margin-bottom: 0.5em;"><span style="font-size: 14px;">SBN 978-3-7757-4340-2</span></p><p style="line-height: 1;">
14921</p><p style="line-height: 1; margin-bottom: 0.5em;"><span style="font-size: 14px;">‘In Waiting,’ is an examination of Athens and its role as a physical stage for the economic crisis. Primarily, I am exploring the interiors of government buildings, institutions, and schools to question how these spaces reflect modern Greek culture and character. The locations I chose are familiar microcosms for Athenians that have now become the public stage for a forced re-negotiation of Greek identity. I am attempting to personally rationalize how the deceptively tedious components of urban life factor into the economic crisis. I am specifically attracted to the particular aesthetic and surreal atmosphere I encounter in these spaces. These interiors also reveal an almost antiquated and bare aesthetic quality; they evoke in their décor a feeling of dependency on and idolization of the past. Spaces, which once seemed banal or unimportant, now reveal nuances of Greek reality and have social and political impl
14921ications. Like the Greek people, these places exist in anticipation of their future.</span></p></div>
14922    <ul class="gallery-images">
14923    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg" alt="IW1" loading="lazy"></li>
14924    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW2.jpg" alt="IW2" loading="lazy"></li>
14925    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW3.jpg" alt="IW3" loading="lazy"></li>
14926    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW4.jpg" alt="IW4" loading="lazy"></li>
14927    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW5.jpg" alt="IW5" loading="lazy"></li>
14928    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW6.jpg" alt="IW6" loading="lazy"></li>
14929    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW7.jpg" alt="IW7" loading="lazy"></li>
14930    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW8.jpg" alt="IW8" loading="lazy"></li>
14931    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW9.jpg" alt="IW9" loading="lazy"></li>
14932    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW10.jpg" alt="IW10" loading="lazy"></li>
14933    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW11.jpg" alt="IW11" loading="lazy"></li>
14934    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW12.jpg" alt="IW12" loading="lazy"></li>
14935    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW13.jpg" alt="IW13" loading="lazy"></li>
14936    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW14.jpg" alt="IW14" loading="lazy"></li>
14937    <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW15.jpg" alt="IW15" loading="lazy"></li>
14938  </ul>
14939    <p><a href="https://eiriniphoto.com/book">View full gallery</a></p>
14940  </article>
14941</noscript>
14942
14943  <div class="global-header-controls">
14944    
14945    <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle">
14946       <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14947        <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
14948          <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"/>
14949          <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"/>
14950        </g>
14951      </svg>
14952    </button>
14953    <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">
14954      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;">
14955        <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"/>
14956      </svg>
14957    </button>
14958  </div>
14959
14960  <div class="container">
14961    <div class="mobile-header">
14962      <div class="mobile-header-content">
14963        <div class="horizontal-header-logo"></div>
14964        <div class="horizontal-menu-container"></div>
14965      </div>
14966      <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar">
14967        <span class="hamburger-icon" aria-hidden="true">
14968          <span class="hamburger-line"></span>
14969          <span class="hamburger-line"></span>
14970          <span class="hamburger-line"></span>
14971        </span>
14972      </button>
14973    </div>
14974    <div id="mobileBackdrop" class="mobile-backdrop"></div>
14975
14976    <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site">
14977      <div class="sidebar-header">
14978</div>
14979
14980  
14981  <div class="edit-controls" data-help-id="edit-toolbar">
14982  <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar">
14983    <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
14984      <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" />
14985    </svg>
14986  </button>
14987  <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar">
14988    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14989      <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"/>
14990    </svg>
14991  </button>
14992  <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add">
14993    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14994      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
14995        <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"/>
14996        <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"/>
14997      </g>
14998    </svg>
14999  </button>
15000  <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm(
15000)" data-custom-tooltip="Add Element">
15001    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15002      <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"/>
15003    </svg>
15004  </button>
15005<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata">
15006<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>
15007</button>
15008<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;">
15009      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15010        <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"/>
15011      </svg>
15012    </button>
15013    
15013<script>
15014
15015// Initialize mobile menu functionality
15016function initMobileMenu() {
15017  const hamburgerBtn = document.getElementById('hamburgerBtn');
15018  const sidebar = document.querySelector('.sidebar');
15019  const mobileBackdrop = document.getElementById('mobileBackdrop');
15020
15021  // --- ADDED CHECK: Only update header if layout is NOT horizontal ---
15022  let currentLayout = 'sidebar'; // Default
15023  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
15024      currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
15025  }
15026
15027  // --- END ADDED CHECK ---
15028
15029  // Ensure backdrop is properly hidden for horizontal layout on page load
15030  if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) {
15031    mobileBackdrop.classList.remove('mobile-open');
15032    mobileBackdrop.classList.remove('active');
15033    // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open
15034    mobileBackdrop.style.display = '';
15035    mobileBackdrop.style.pointerEvents = '';
15036    mobileBackdrop.style.opacity = '';
15037    mobileBackdrop.style.visibility = '';
15038  }
15039
15040  // Toggle mobile menu when hamburger is clicked
15041  hamburgerBtn.addEventListener('click', function(e) {
15042    e.stopPropagation();
15043    toggleMobileMenu(); // Assumes this function exists
15044  });
15045
15046  // Close mobile menu when backdrop is clicked
15047  if (mobileBackdrop) {
15048    mobileBackdrop.addEventListener('click', function() {
15049      closeMobileMenu(); // Assumes this function exists
15050    });
15051  }
15052
15053  // Escape closes mobile menu (keyboard / SR)
15054  document.addEventListener('keydown', function(e) {
15055    if (e.key !== 'Escape') return;
15056    const sb = document.querySelector('.sidebar');
15057    if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') {
15058      closeMobileMenu();
15059    }
15060  });
15061
15062  // Handle window resize events to ensure proper state
15063  window.addEventListener('resize', function() {
15064    // If we're not on mobile anymore, remove mobile-open class
15065    if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
15066      closeMobileMenu(); // Assumes this function exists
15067    }
15068  });
15069}
15070
15071document.addEventListener('keydown', function(e) {
15072  // Check for caret (Shift+6)
15073  if (e.key === '^' || (e.key === '6' && e.shiftKey)) {
15074    // Only work in edit mode
15075    if (window.isInEditMode && window.isInEditMode()) {
15076      console.log('Import shortcut detected');
15077      toggleImportClassicForm();
15078      e.preventDefault();
15079    }
15080  }
15081});
15082
15083// Enhanced tooltip management with automatic timeouts
15084
15085// Store active tooltip timeouts by ID
15086window.tooltipTimeouts = {};
15087
15088function showTooltip(trigger) {
15089    const tooltipText = trigger.getAttribute('data-custom-tooltip');
15090    if (!tooltipText) return;
15091
15092    // Get or create the main tooltip container
15093    let container = document.getElementById('tooltip-container');
15094    if (!container) {
15095        console.log("Tooltip container not found, creating one.");
15096        container = document.createElement('div'); // Assign to 'container'
15097        container.id = 'tooltip-container';
15098        // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc.
15099        document.body.appendChild(container);
15100    }
15101
15102    const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`;
15103
15104    // Remove any existing tooltip for this trigger
15105    const existingTooltip = document.getElementById(tooltipId);
15106    if (existingTooltip) {
15107        existingTooltip.remove();
15108    }
15109
15110    // Clear any existing timeout for this tooltip
15111    if (!window.tooltipTimeouts) {
15112        window.tooltipTimeouts = {}; // Initialize if it doesn't exist
15113    }
15114    
15115    if (window.tooltipTimeouts[tooltipId]) {
15116        clearTimeout(window.tooltipTimeouts[tooltipId]);
15117        delete window.tooltipTimeouts[tooltipId];
15118    }
15119
15120    // Create tooltip element
15121    const tooltip = document.createElement('div');
15122    tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip
15123    tooltip.id = tooltipId;
15124    tooltip.textContent = tooltipText;
15125
15126    // Add to container (now 'container' is guaranteed to be an element)
15127    container.appendChild(tooltip);
15128
15129    // Position tooltip
15130    if (typeof positionTooltip === 'function') {
15131        positionTooltip(tooltip, trigger);
15132    } else {
15133        console.error("positionTooltip function is not defined.");
15134        tooltip.remove(); // Clean up if we can't position
15135        return;
15136    }
15137
15138    // Show with animation (slight delay for CSS transition)
15139    setTimeout(() => {
15140        tooltip.style.opacity = '1';
15141        // The transform for animation should be handled by CSS transitions
15142        // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); }
15143        // Or ensure positionTooltip sets final transform if it needs to override CSS.
15144        // For now, assuming CSS handles the animated transform from its initial state.
15145    }, 10);
15146
15147    trigger.setAttribute('data-active-tooltip', tooltipId);
15148
15149    // Set automatic timeout to hide after 3 seconds
15150    window.tooltipTimeouts[tooltipId] = setTimeout(() => {
15151        hideTooltip(trigger); // Ensure hideTooltip function exists
15152    }, 3000);
15153    
15154    // Also set up a global timer to ensure all tooltips are dismissed
15155    if (window.globalTooltipTimer) {
15156        clearTimeout(window.globalTooltipTimer);
15157    }
15158    window.globalTooltipTimer = setTimeout(() => {
15159        // Force hide all visible tooltips after 3 seconds
15160        const allTooltips = document.querySelectorAll('.tooltip');
15161        allTooltips.forEach(tooltip => {
15162            if (tooltip.style.opacity !== '0') {
15163                tooltip.style.opacity = '0';
15164                setTimeout(() => {
15165                    if (tooltip.parentNode) {
15166                        tooltip.parentNode.removeChild(tooltip);
15167                    }
15168                }, 200);
15169            }
15170        });
15171        // Clear all timeout references
15172        if (window.tooltipTimeouts) {
15173            for (const id in window.tooltipTimeouts) {
15174                clearTimeout(window.tooltipTimeouts[id]);
15175            }
15176            window.tooltipTimeouts = {};
15177        }
15178    }, 3000);
15179}
15180
15181// Function to hide a tooltip
15182function hideTooltip(trigger) {
15183  const tooltipId = trigger.getAttribute('data-active-tooltip');
15184  if (!tooltipId) return;
15185  
15186  const tooltip = document.getElementById(tooltipId);
15187  if (tooltip) {
15188    // Fade out
15189    tooltip.style.opacity = '0';
15190    tooltip.style.transform = 'translateX(-50%) translateY(0)';
15191    
15192    // Remove after animation
15193    setTimeout(() => {
15194      if (tooltip.parentNode) {
15195        tooltip.parentNode.removeChild(tooltip);
15196      }
15197    }, 200);
15198  }
15199  
15200  // Clear any active timeout for this tooltip
15201  if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) {
15202    clearTimeout(window.tooltipTimeouts[tooltipId]);
15203    delete window.tooltipTimeouts[tooltipId];
15204  }
15205  
15206  // Clear reference
15207  trigger.removeAttribute('data-active-tooltip');
15208  
15209  // If this was the last tooltip, clear the global timer
15210  const remainingTooltips = document.querySelectorAll('.tooltip');
15211  if (remainingTooltips.length === 0 && window.globalTooltipTimer) {
15212    clearTimeout(window.globalTooltipTimer);
15213    window.globalTooltipTimer = null;
15214  }
15215}
15216
15217// Function to initialize tooltips across the document
15218function initTooltipSystem() {
15219  // Check if container already exists
15220  let tooltipContainer = document.getElementById('tooltip-container');
15221  
15222  // Create it if it doesn't exist
15223  if (!tooltipContainer) {
15224    tooltipContainer = document.createElement('div');
15225    tooltipContainer.id = 'tooltip-container';
15226    document.body.appendChild(tooltipContainer);
15227  }
15228  
15229  // Find all elements with data-custom-tooltip attribute
15230  const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]');
15231  
15232  // Add event listeners to each trigger
15233  tooltipTriggers.forEach(trigger => {
15234    // Skip if already initialized
15235    if (trigger.hasAttribute('data-custom-tooltip-initialized')) return;
15236    
15237    // Mark as initialized
15238    trigger.setAttribute('data-custom-tooltip-initialized', 'true');
15239    
15240    // MouseEnter: Show tooltip
15241    trigger.addEventListener('mouseenter', () => {
15242      showTooltip(trigger);
15243    });
15244    
15245    // MouseLeave: Hide tooltip
15246    trigger.addEventListener('mouseleave', () => {
15247      hideTooltip(trigger);
15248    });
15249    
15250    // Click: Hide tooltip immediately
15251    trigger.addEventListener('click', () => {
15252      hideTooltip(trigger);
15253    });
15254    
15255    // Also handle focus/blur for accessibility
15256    trigger.addEventListener('focus', () => {
15257      showTooltip(trigger);
15258    });
15259    
15260    trigger.addEventListener('blur', () => {
15261      hideTooltip(trigger);
15262    });
15263  });
15264  
15265  // Set up a failsafe function to clear any orphaned tooltips
15266  window.clearAllTooltips = function() {
15267    const container = document.getElementById('tooltip-container');
15268    if (container) {
15269      container.innerHTML = '';
15270    }
15271    
15272    // Clear all timeouts
15273    if (window.tooltipTimeouts) {
15274      for (const id in window.tooltipTimeouts) {
15275        clearTimeout(window.tooltipTimeouts[id]);
15276      }
15277      window.tooltipTimeouts = {};
15278    }
15279    
15280    // Clear global timer
15281    if (window.globalTooltipTimer) {
15282      clearTimeout(window.globalTooltipTimer);
15283      window.globalTooltipTimer = null;
15284    }
15285    
15286    // Clear all data-active-tooltip attributes
15287    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15288      el.removeAttribute('data-active-tooltip');
15289    });
15290  };
15291  
15292  // Initialize new tooltips when DOM changes
15293  setupMutationObserver();
15294  
15295  // Add a global function to manually dismiss all tooltips (for testing)
15296  window.dismissAllTooltips = function() {
15297    const allTooltips = document.querySelectorAll('.tooltip');
15298    allTooltips.forEach(tooltip => {
15299      if (tooltip.style.opacity !== '0') {
15300        tooltip.style.opacity = '0';
15301        setTimeout(() => {
15302          if (tooltip.parentNode) {
15303            tooltip.parentNode.removeChild(tooltip);
15304          }
15305        }, 200);
15306      }
15307    });
15308    
15309    // Clear all timeout references
15310    if (window.tooltipTimeouts) {
15311      for (const id in window.tooltipTimeouts) {
15312        clearTimeout(window.tooltipTimeouts[id]);
15313      }
15314      window.tooltipTimeouts = {};
15315    }
15316    
15317    // Clear global timer
15318    if (window.globalTooltipTimer) {
15319      clearTimeout(window.globalTooltipTimer);
15320      window.globalTooltipTimer = null;
15321    }
15322    
15323    // Clear all data-active-tooltip attributes
15324    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15325      el.removeAttribute('data-active-tooltip');
15326    });
15327  };
15328}
15329
15330function positionTooltip(tooltip, trigger) {
15331    const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates
15332
15333    // To accurately get tooltip dimensions, temporarily make it visible but off-screen.
15334    // The #tooltip-container is position:fixed, so the tooltip's absolute positioning
15335    // will be relative to the viewport.
15336    tooltip.style.visibility = 'hidden'; 
15337    tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display
15338    // No need to change position to 'absolute' here if its CSS is already 'position: absolute;'
15339    // within the 'fixed' #tooltip-container.
15340
15341    const tooltipHeight = tooltip.offsetHeight;
15342    const tooltipWidth = tooltip.offsetWidth;
15343
15344    // Restore visibility; opacity transition will handle the fade-in.
15345    tooltip.style.visibility = ''; 
15346    // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding
15347
15348    // Check if tooltip should be positioned to the right (for conflict indicator)
15349    const tooltipPosition = trigger.getAttribute('data-tooltip-position');
15350    if (tooltipPosition === 'right') {
15351        // Position to the right of the trigger
15352        const viewportWidth = window.innerWidth;
15353        const viewportHeight = window.innerHeight;
15354        let left = triggerRect.right + 10; // 10px gap from the right edge of trigger
15355        let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered
15356        
15357        // Recalculate tooltip dimensions after allowing text wrap
15358        tooltip.style.maxWidth = '200px';
15359        tooltip.style.whiteSpace = 'normal';
15360        tooltip.style.wordWrap = 'break-word';
15361        const tooltipWidthAfterWrap = tooltip.offsetWidth;
15362        const tooltipHeightAfterWrap = tooltip.offsetHeight;
15363        
15364        // If tooltip goes off-screen to the right, try positioning to the left of the button
15365        if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15366            left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button
15367            // If it still goes off the left edge, position it at the left edge with padding
15368            if (left < 5) {
15369                left = 5; // Keep it at least 5px from the left edge
15370                // The tooltip will wrap to fit within the available space
15371                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15372            }
15373        } else {
15374            // Tooltip fits on the right, but make sure it doesn't go off-screen
15375            if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15376                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15377            }
15378        }
15379        
15380        // Recalculate after max-width adjustment
15381        const finalTooltipWidth = tooltip.offsetWidth;
15382        const finalTooltipHeight = tooltip.offsetHeight;
15383        
15384        // Ensure tooltip doesn't go off-screen on the right side
15385        if (left + finalTooltipWidth > viewportWidth - 5) {
15386            left = viewportWidth - finalTooltipWidth - 5;
15387        }
15388        
15389        // Adjust if tooltip goes off-screen vertically
15390        if (top < 5) {
15391            top = 5;
15392        } else if (top + finalTooltipHeight > viewportHeight - 5) {
15393            top = viewportHeight - finalTooltipHeight - 5;
15394        }
15395        
15396        tooltip.style.left = `${left}px`;
15397        tooltip.style.top = `${top}px`;
15398        tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning
15399        return;
15400    }
15401
15402    // Calculate desired horizontal center point of the trigger
15403    const triggerCenter = triggerRect.left + triggerRect.width / 2;
15404    
15405    // Calculate tooltip's left position to center it under/over trigger's center
15406    let left = triggerCenter - tooltipWidth / 2;
15407
15408    // Default position: above the trigger
15409    // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset
15410    let top = triggerRect.top - tooltipHeight - 10; 
15411    tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS
15412
15413    // Check if it goes off-screen at the top (add a small buffer, e.g., 5px)
15414    if (top < 5) { 
15415        // Position below the trigger
15416        top = triggerRect.bottom + 10; // 10px gap (arrow height + small space)
15417        tooltip.classList.add('tooltip-bottom');
15418    }
15419
15420    // Boundary checks for left position to prevent going off-screen horizontally
15421    const viewportWidth = window.innerWidth;
15422    if (left < 5) { // 5px buffer from left edge
15423        left = 5; 
15424    } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge
15425        left = viewportWidth - tooltipWidth - 5;
15426    }
15427
15428    tooltip.style.left = `${left}px`;
15429    tooltip.style.top = `${top}px`;
15430
15431    // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering.
15432    // Since JS now calculates the exact 'left' for centering,
15433    // we can clear any such transform that might interfere.
15434    // However, if your CSS animation relies on a transform, adjust accordingly.
15435    // If your CSS only uses opacity for transition, this is fine:
15436    tooltip.style.transform = ''; 
15437}
15438
15439// Watch for new tooltip triggers added to the DOM
15440function setupMutationObserver() {
15441  // Check if MutationObserver is available
15442  if (!window.MutationObserver) return;
15443  
15444  // Create observer instance
15445  const observer = new MutationObserver((mutations) => {
15446    let shouldInit = false;
15447    
15448    // Check if relevant nodes were added
15449    mutations.forEach(mutation => {
15450      if (mutation.type === 'childList') {
15451        mutation.addedNodes.forEach(node => {
15452          // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip
15453          if (node.nodeType === 1) { // ELEMENT_NODE
15454            if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) {
15455              shouldInit = true;
15456            } else if (node.querySelectorAll) {
15457              const tooltips = node.querySelectorAll('[data-custom-tooltip]');
15458              if (tooltips.length) shouldInit = true;
15459            }
15460          }
15461        });
15462      }
15463    });
15464    
15465    // If relevant nodes found, reinitialize tooltips
15466    if (shouldInit) {
15467      initTooltipSystem();
15468    }
15469  });
15470  
15471  // Start observing the entire document with the configured parameters
15472  observer.observe(document.body, { 
15473    childList: true, 
15474    subtree: true 
15475  });
15476}
15477
15478// Initialize when DOM is ready
15479document.addEventListener('DOMContentLoaded', initTooltipSystem);
15480
15481// Also initialize immediately if document already loaded
15482if (document.readyState === 'complete' || document.readyState === 'interactive') {
15483  initTooltipSystem();
15484}
15485
15486// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck
15487setInterval(() => {
15488  // Get all visible tooltips
15489  const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]');
15490  
15491  // If any are found, check how long they've been visible
15492  if (visibleTooltips.length > 0) {
15493    console.log('Found visible tooltips during cleanup check, clearing them');
15494    window.clearAllTooltips();
15495  }
15496}, 5000); // 
15497
15498// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes.
15499var _mobileMenuFocusTrapHandler = null;
15500
15501function getMobileMenuFocusables(sidebar) {
15502  if (!sidebar) return [];
15503  return Array.prototype.filter.call(
15504    sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15504([tabindex="-1"])'),
15505    function (el) {
15506      if (el.getAttribute('aria-hidden') === 'true') return false;
15507      var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
15508      if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
15509      return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement;
15510    }
15511  );
15512}
15513
15514function releaseMobileMenuFocusTrap() {
15515  if (_mobileMenuFocusTrapHandler) {
15516    document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15517    _mobileMenuFocusTrapHandler = null;
15518  }
15519}
15520
15521function installMobileMenuFocusTrap(sidebar) {
15522  releaseMobileMenuFocusTrap();
15523  if (!sidebar) return;
15524  _mobileMenuFocusTrapHandler = function (event) {
15525    if (event.key !== 'Tab') return;
15526    var sb = document.querySelector('.sidebar');
15527    if (!sb || !sb.classList.contains('mobile-open')) return;
15528    var focusables = getMobileMenuFocusables(sb);
15529    var hamburger = document.getElementById('hamburgerBtn');
15530    // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer
15531    var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables;
15532    if (!cycle.length) return;
15533    var first = cycle[0];
15534    var last = cycle[cycle.length - 1];
15535    if (event.shiftKey) {
15536      if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) {
15537        event.preventDefault();
15538        last.focus();
15539      }
15540    } else if (document.activeElement === last) {
15541      event.preventDefault();
15542      first.focus();
15543    }
15544  };
15545  document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15546}
15547
15548// Toggle mobile menu state
15549function toggleMobileMenu() {
15550const sidebar = document.querySelector('.sidebar');
15551const hamburgerBtn = document.getElementById('hamburgerBtn');
15552const mobileBackdrop = document.getElementById('mobileBackdrop');
15553
15554// Check if we're in horizontal layout mode
15555const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15556
15557sidebar.classList.toggle('mobile-open');
15558hamburgerBtn.classList.toggle('active');
15559const isOpen = sidebar.classList.contains('mobile-open');
15560hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
15561
15562if (mobileBackdrop) {
15563  if (isHorizontalLayout) {
15564    // For horizontal layout on mobile, use normal backdrop (tap outside to close)
15565    // Clear any inline overrides so CSS can control visibility
15566    mobileBackdrop.style.display = '';
15567    mobileBackdrop.style.pointerEvents = '';
15568    mobileBackdrop.style.opacity = '';
15569    mobileBackdrop.style.visibility = '';
15570    mobileBackdrop.classList.toggle('mobile-open', isOpen);
15571    setTimeout(() => {
15572      mobileBackdrop.classList.toggle('active', isOpen);
15573    }, 10);
15574  } else {
15575    // For other layouts, use normal backdrop behavior
15576    mobileBackdrop.classList.toggle('mobile-open');
15577    // Slight delay to show opacity transition
15578    setTimeout(() => {
15579      mobileBackdrop.classList.toggle('active', isOpen);
15580    }, 10);
15581  }
15582}
15583
15584// Prevent body scrolling when menu is open
15585document.body.style.overflow = isOpen ? 'hidden' : '';
15586
15587if (isOpen) {
15588  // Trap Tab only — do not steal focus on mouse/touch open (avoids focus rings / scroll jumps)
15589  installMobileMenuFocusTrap(sidebar);
15590} else {
15591  releaseMobileMenuFocusTrap();
15592  // Restore focus only if keyboard focus was inside the drawer
15593  if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15594    try { hamburgerBtn.focus(); } catch (e) {}
15595  }
15596}
15597}
15598
15599// Close the mobile menu
15600function closeMobileMenu() {
15601const sidebar = document.querySelector('.sidebar');
15602const hamburgerBtn = document.getElementById('hamburgerBtn');
15603const mobileBackdrop = document.getElementById('mobileBackdrop');
15604const wasOpen = sidebar && sidebar.classList.contains('mobile-open');
15605
15606// Check if we're in horizontal layout mode
15607const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15608
15609sidebar.classList.remove('mobile-open');
15610hamburgerBtn.classList.remove('active');
15611if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false');
15612
15613if (mobileBackdrop) {
15614  if (isHorizontalLayout) {
15615    // For horizontal layout, clear inline overrides and remove mobile-open
15616    mobileBackdrop.style.display = '';
15617    mobileBackdrop.style.pointerEvents = '';
15618    mobileBackdrop.style.opacity = '';
15619    mobileBackdrop.style.visibility = '';
15620    mobileBackdrop.classList.remove('mobile-open');
15621    mobileBackdrop.classList.remove('active');
15622  } else {
15623    // For other layouts, use normal backdrop behavior
15624    mobileBackdrop.classList.remove('active');
15625    setTimeout(() => {
15626      mobileBackdrop.classList.remove('mobile-open');
15627    }, 300); // Match transition duration
15628  }
15629}
15630
15631// Re-enable body scrolling
15632document.body.style.overflow = '';
15633releaseMobileMenuFocusTrap();
15634// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged
15635if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15636  try { hamburgerBtn.focus(); } catch (e) {}
15637}
15638}
15639
15640function updateMobileHeaderContent() {
15641    const currentLayout = typeof getEffectiveMenuLayout === 'function'
15642        ? getEffectiveMenuLayout()
15643        : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
15644            ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'
15645            : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar'));
15646
15647    const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint
15648
15649    const mobileHeaderContent = document.querySelector('.mobile-header-content');
15650    if (!mobileHeaderContent) {
15651        console.warn("updateMobileHeaderContent: .mobile-header-content element not found.");
15652        return;
15653    }
15654    
15655    // On mobile, center the content unless Space Colony (left-aligned title)
15656    if (isMobileView) {
15657        const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme();
15658        mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15659        // Also ensure logo-menu-container doesn't grow
15660        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
15661        if (logoMenuContainer) {
15662            logoMenuContainer.style.flexGrow = '0';
15663            logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15664        }
15665    }
15666
15667    console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`);
15668
15669    // Clear previous generic mobile content (like centered logo/title)
15670    const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image');
15671    const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title');
15672    if (existingMobileImage) {
15673        // The img is always appended directly (never wrapped in an <a>). Remove it straight.
15674        // Also handle any legacy <a>-wrapped images left from older deployments.
15675        const imageParent = existingMobileImage.parentElement;
15676        if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) {
15677            imageParent.remove();
15678        } else {
15679            existingMobileImage.remove();
15680        }
15681    }
15682    // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach
15683    Array.from(mobileHeaderContent.children).forEach(function(child) {
15684        if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) {
15685            child.remove();
15686        }
15687    });
15688    if (existingMobileText) existingMobileText.remove();
15689    
15690    if (currentLayout === 'horizontal' && !isMobileView) {
15691        // Desktop Horizontal Layout:
15692        // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo.
15693        // The menu items are in .horizontal-menu-container.
15694        // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo).
15695        mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) {
15696            el.remove();
15697        });
15698        console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available.");
15699        if (typeof renderHorizontalMenu === 'function') {
15700            // Wait for sidebar elements to be available before rendering
15701            const waitForSidebarElements = (attempts = 0) => {
15702                const maxAttempts = 10;
15703                if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15704                    console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu.");
15705                    renderHorizontalMenu();
15706                } else if (attempts < maxAttempts) {
15707                    const attemptNum = attempts + 1;
15708                    console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")...");
15709                    setTimeout(() => waitForSidebarElements(attempts + 1), 100);
15710                } else {
15711                    console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway.");
15712                    renderHorizontalMenu();
15713                }
15714            };
15715            waitForSidebarElements();
15716        }
15717        return; 
15718    }
15719    
15720    // For Horizontal layout on mobile, we need to clear the horizontal menu items
15721    // and show only the mobile header logo/title
15722    if (currentLayout === 'horizontal' && isMobileView) {
15723        console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo.");
15724        
15725        // Clear horizontal menu items from mobile header
15726        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15726-container');
15727        if (horizontalMenuContainer) {
15728            console.log("updateMobileHeaderContent: Clearing horizontal menu container");
15729            horizontalMenuContainer.innerHTML = '';
15730        } else {
15731            console.log("updateMobileHeaderContent: No horizontal menu container found to clear");
15732        }
15733        
15734        // Clear horizontal header logo from mobile header
15735        const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo');
15736        if (horizontalHeaderLogo) {
15737            console.log("updateMobileHeaderContent: Clearing horizontal header logo");
15738            horizontalHeaderLogo.innerHTML = '';
15739        } else {
15740            console.log("updateMobileHeaderContent: No horizontal header logo found to clear");
15741        }
15742        
15743        // Also clear any existing horizontal menu items that might be directly in mobile-header-content
15744        const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item');
15745        if (existingHorizontalMenuItems.length > 0) {
15746            console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items');
15747            existingHorizontalMenuItems.forEach(item => item.remove());
15748        }
15749    }
15750    
15751    // For Sidebar, Top, OR Horizontal layout on mobile:
15752    // Populate .mobile-header-content with the first sidebar element (logo/title).
15753    // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout.
15754
15755    console.log("updateMobileHeaderContent: Populating with first sidebar element.");
15756    
15757    // For horizontal layout on mobile, use the header-specific function to get logo/title
15758    // For other layouts, use the general function
15759    let firstElement;
15760    if (currentLayout === 'horizontal' && isMobileView) {
15761        firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically
15762        console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout");
15763    } else {
15764        firstElement = getFirstSidebarElement(); // Gets any first element
15765        console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts");
15766    }
15767
15768    if (firstElement) {
15769        console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement));
15770        
15771        if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) {
15772            console.log('updateMobileHeaderContent: Creating mobile header image');
15773            const img = document.createElement('img');
15774            img.src = firstElement.imageUrl || firstElement.imageData;
15775            img.alt = resolveBrandLogoAlt(firstElement);
15776            img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed
15777
15778            let mobileDisplayNode = img;
15779            if (firstElement.colorizeWithSiteColors &&
15780                window.SidebarManager &&
15781                typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
15782              const buildData = Object.assign({}, firstElement);
15783              if (!buildData.imageWidth) {
15784                buildData.imageWidth = 'auto';
15785              }
15786              mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
15787            }
15788
15789            // Opt-in per-image padding wrapper (no-op when unset)
15790            if (window.SidebarManager &&
15791                typeof window.SidebarManager._wrapWithImagePadding === 'function') {
15792              mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement);
15793            }
15794            
15795            // Handle linking: always append the img directly so existing CSS centering is
15796            // not disrupted by an <a> wrapper (which the many !important positioning rules
15797            // were not written to handle). Navigation is wired via a click handler.
15798            if (firstElement.linkType && firstElement.linkType !== 'none') {
15799                let finalLinkHref = '#';
15800                let isValidLink = false;
15801                const linkTarget = firstElement.linkTarget || '_self';
15802                
15803                if (firstElement.linkType === 'external' && firstElement.linkUrl) {
15804                    finalLinkHref = firstElement.linkUrl;
15805                    isValidLink = true;
15806                } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) {
15807                    const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15808                    const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId));
15809                    
15810                    if (targetPage) {
15811                        const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
15812                        if (isPreviewMode) {
15813                            const pathParts = window.location.pathname.split('/').filter(Boolean);
15814                            const siteGuid = pathParts[0];
15815                            if (siteGuid && targetPage.slug) {
15816                                finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
15817                                isValidLink = true;
15818                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15819                                finalLinkHref = targetPage.url;
15820                                isValidLink = true;
15821                            }
15822                        } else {
15823                            if (targetPage.slug) {
15824                                finalLinkHref = '/' + targetPage.slug;
15825                                isValidLink = true;
15826                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15827                                finalLinkHref = targetPage.url;
15828                                isValidLink = true;
15829                            }
15830                        }
15831                    }
15832                }
15833                
15834                if (isValidLink) {
15835                    mobileDisplayNode.style.cursor = 'pointer';
15836                    const capturedHref = finalLinkHref;
15837                    const capturedTarget = linkTarget;
15838                    const capturedLinkType = firstElement.linkType;
15839                    const capturedPageId = firstElement.linkPageId;
15840                    
15841                    mobileDisplayNode.addEventListener('click', (e) => {
15842                        e.preventDefault();
15843                        if (capturedLinkType === 'internal' && capturedPageId) {
15844                            const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15845                            const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId));
15846                            if (targetPage) {
15847                                if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
15848                                    window.loadPage(targetPage.id);
15849                                } else if (typeof window.loadGallery === 'function') {
15850                                    window.loadGallery(targetPage.id);
15851                                } else {
15852                                    window.location.href = capturedHref;
15853                                }
15854                            } else {
15855                                window.location.href = capturedHref;
15856                            }
15857                        } else {
15858                            if (capturedTarget === '_blank') {
15859                                window.open(capturedHref, '_blank');
15860                            } else {
15861                                window.location.href = capturedHref;
15862                            }
15863                        }
15864                    });
15865                }
15866            }
15867            mobileHeaderContent.appendChild(mobileDisplayNode);
15868        } else if (firstElement.type === 'text' && firstElement.textContent) {
15869            console.log('updateMobileHeaderContent: Creating mobile header text');
15870            const textDiv = document.createElement('div');
15871            textDiv.className = 'mobile-header-text'; // For specific styling
15872            textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML
15873            const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
15874            if (titleEl && !titleEl.classList.contains('site-title-text')) {
15875              titleEl.classList.add('site-title-text');
15876            }
15877            mobileHeaderContent.appendChild(textDiv);
15878        } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) {
15879            console.log('updateMobileHeaderContent: Creating mobile header menu title');
15880            // Fallback for 'menu' type, find the first visible gallery title
15881            const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer);
15882            const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu');
15883            const titleDiv = document.createElement('div');
15884            titleDiv.className = 'mobile-header-menu-title'; // For specific styling
15885            titleDiv.textContent = titleText;
15886            mobileHeaderContent.appendChild(titleDiv);
15887        } else { // Default fallback
15888            console.log('updateMobileHeaderContent: Creating mobile header title (fallback)');
15889            const titleDiv = document.createElement('div');
15890            titleDiv.className = 'mobile-header-title'; // For specific styling
15891            titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu';
15892            mobileHeaderContent.appendChild(titleDiv);
15893        }
15894    } else {
15895        // Fallback if no sidebar elements exist
15896         const defaultTitle = document.createElement('div');
15897         defaultTitle.className = 'mobile-header-title';
15898         defaultTitle.textContent = 'Menu';
15899         mobileHeaderContent.appendChild(defaultTitle);
15900        console.log("updateMobileHeaderContent: No first sidebar element found, showing default title.");
15901    }
15902
15903    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15904      syncContentMarginToHorizontalHeader();
15905    }
15906}
15907
15908// Helper function to get the first element in the sidebar
15909function getFirstSidebarElement() {
15910// Check if window.SidebarManager exists and has elements
15911if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15912// Filter to only visible elements
15913const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer');
15914
15915if (visibleElements.length > 0) {
15916  // Sort by position and return the first one
15917  return visibleElements.sort((a, b) => a.position - b.position)[0];
15918}
15919}
15920
15921// If no sidebar elements, return first gallery as fallback
15922if (galleries.length > 0) {
15923const firstVisibleGallery = galleries.find(g => g.visible !== false);
15924if (firstVisibleGallery) {
15925  return {
15926    type: 'menu',
15927    title: firstVisibleGallery.title
15928  };
15929}
15930}
15931
15932// Default fallback
15933return {
15934type: 'text',
15935title: 'Menu'
15936};
15937}
15938// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE
15939// Just replace the updateMobileTitle function and add our initialization code
15940
15941// Replace the existing updateMobileTitle function with this one
15942function updateMobileTitle() {
15943// Always call our implementation that shows the top sidebar element
15944updateMobileHeaderContent();
15945}
15946
15947// Add window resize listener to handle mobile header content updates
15948window.addEventListener('resize', () => {
15949  // Debounce the resize event to avoid excessive calls
15950  clearTimeout(window.resizeTimeout);
15951  window.resizeTimeout = setTimeout(() => {
15952    if (typeof updateMobileHeaderContent === 'function') {
15953      updateMobileHeaderContent();
15954    }
15955    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
15956      applyHorizontalLogoMenuLayoutStyles();
15957    }
15958    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15959      syncContentMarginToHorizontalHeader();
15960    }
15961  }, 250);
15962});
15963
15964//separate initialization function that doesn't replace the original initMobileMenu
15965function initTopElementDisplay() {
15966console.log("Initializing top element display");
15967
15968// Run immediately
15969updateMobileHeaderContent();
15970
15971if (typeof syncContentMarginToHorizontalHeader === 'function') {
15972  setTimeout(syncContentMarginToHorizontalHeader, 150);
15973}
15974
15975// Also hook into loadGallery to ensure it runs whenever galleries change
15976const originalLoadGallery = window.loadGallery;
15977if (typeof originalLoadGallery === 'function') {
15978window.loadGallery = function(id, event, options) {
15979  // Forward all args (including post-save { forceReload: true }) to the underlying loader
15980  const result = originalLoadGallery(id, event, options);
15981  updateMobileHeaderContent();
15982  return result;
15983};
15984}
15985}
15986
15987
15988// Initialize our functionality after a short delay to ensure SidebarManager is ready
15989document.addEventListener('DOMContentLoaded', function() {
15990// Short delay to ensure SidebarManager is initialized
15991setTimeout(initTopElementDisplay, 100);
15992
15993// Also run a bit later to catch any late-loading elements
15994setTimeout(updateMobileHeaderContent, 500);
15995});
15996
15997// Run immediately if document is already loaded
15998if (document.readyState === 'complete' || document.readyState === 'interactive') {
15999setTimeout(initTopElementDisplay, 100);
16000}
16001
16002// Add a final fallback to ensure gallery visibility on window load
16003window.addEventListener('load', function() {
16004  setTimeout(ensureGalleryVisibility, 2000);
16005});
16006
16007
16008(function() {
16009  /**
16010   * EnhancedGallerySystem - Comprehensive gallery script and state management
16011   */
16012  class EnhancedGallerySystem {
16013    constructor() {
16014      // Core state tracking
16015      this.initialized = false;
16016      this.initPromise = null;
16017      this.isLoading = false;
16018      this.currentGalleryId = null;
16019      this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames)
16020
16021      // Track loaded resources
16022      this.loadedScripts = new Set();
16023      this.loadedStyles = new Set();
16024      
16025      // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues)
16026      const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
16027      this.essentialResources = {
16028        scripts: [
16029          { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' },
16030          { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' }
16031        ],
16032        styles: [
16033          { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }
16034        ]
16035      };
16036      
16037      // Keep track of original initialization functions
16038      this.originalInitFunctions = {};
16039      
16040      // Keep reference to gallery state between changes
16041      this.previousGalleryState = null;
16042      
16043      // Track last initialization time per gallery ID to prevent duplicates
16044      this.lastInitializationTime = new Map();
16045      this._loadInProgress = null;
16046      this._loadingGalleryId = null;
16047    }
16048
16049    /**
16050 * Setup the correct gallery container structure
16051 * @param {HTMLElement} container - The main container element
16052 * @returns {boolean} - Success indicator
16053 */
16054setupCorrectGalleryContainer(container) {
16055  console.log('EnhancedGallerySystem: Setting up correct gallery container structure');
16056  
16057  // Validate container
16058  if (!container) {
16059    console.error('EnhancedGallerySystem: Main gallery container is null');
16060    return false;
16061  }
16062  
16063  try {
16064    // Clear the container
16065    container.innerHTML = '';
16066    
16067    // Create the exact container structure expected by the gallery script
16068    const neonContainer = document.createElement('div');
16069    neonContainer.id = 'neon-gallery-container';
16070    neonContainer.style.width = '100%';
16071    neonContainer.style.height = '100%';
16072    neonContainer.style.position = 'relative';
16073    
16074    // Add the container to the DOM
16075    container.appendChild(neonContainer);
16076    
16077    // Set visibility
16078    container.style.opacity = '1';
16079    container.style.display = 'block';
16080    
16081    console.log('EnhancedGallerySystem: Gallery container structure created');
16082    return true;
16083  } catch (error) {
16084    console.error('EnhancedGallerySystem: Error setting up gallery container:', error);
16085    return false;
16086  }
16087}
16088
16089    /**
16090     * Ensure #neon-gallery-container exists before gallery scripts execute.
16091     * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}.
16092     */
16093    ensureGalleryContainerPlaceholder(container) {
16094      if (document.getElementById('neon-gallery-container')) {
16095        return document.getElementById('neon-gallery-container');
16096      }
16097      const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement;
16098      if (!host) {
16099        return null;
16100      }
16101      const neonContainer = document.createElement('div');
16102      neonContainer.id = 'neon-gallery-container';
16103      neonContainer.className = 'gallery-direct-content';
16104      neonContainer.style.width = '100%';
16105      neonContainer.style.height = '100%';
16106      neonContainer.style.position = 'relative';
16107      host.appendChild(neonContainer);
16108      console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load');
16109      return neonContainer;
16110    }
16111
16112    /**
16113     * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed.
16114     */
16115    async ensureNeonGalleryUtilsReady(container) {
16116      const hasLoadGallerySettings = () =>
16117        typeof window.neonGalleryUtils?.loadGallerySettings === 'function';
16118
16119      if (hasLoadGallerySettings()) {
16120        return true;
16121      }
16122
16123      const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main');
16124      if (!mainScript) {
16125        return false;
16126      }
16127
16128      console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing — reloading main script');
16129      this.ensureGalleryContainerPlaceholder(container);
16130
16131      document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16132        if (el.parentNode) el.parentNode.removeChild(el);
16133      });
16134      this.loadedScripts.delete('neon-gallery-main');
16135      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16136
16137      await new Promise((resolve, reject) => {
16138        const scriptElement = document.createElement('script');
16139        scriptElement.id = 'neon-gallery-main';
16140        scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`;
16141        scriptElement.async = false;
16142        scriptElement.onload = () => {
16143          this.loadedScripts.add('neon-gallery-main');
16144          resolve();
16145        };
16146        scriptElement.onerror = (err) => reject(err);
16147        document.body.appendChild(scriptElement);
16148      });
16149
16150      for (let i = 0; i < 50; i++) {
16151        if (hasLoadGallerySettings()) {
16152          return true;
16153        }
16154        await new Promise(r => setTimeout(r, 100));
16155      }
16156
16157      return hasLoadGallerySettings();
16158    }
16159
16160    /**
16161     * Preload styles only — defer script loading until a gallery load provides a container.
16162     */
16163    preloadStylesOnly() {
16164      return this.loadStyles();
16165    }
16166
16167    /**
16168     * Reset initialization tracking when gallery scripts are removed from the DOM
16169     * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and
16170     * subsequent loadGallery calls hang with isLoading:true, initialized:false.
16171     */
16172    resetInitializationState() {
16173      console.log('EnhancedGallerySystem: Resetting initialization state');
16174      this.initPromise = null;
16175      this.initialized = false;
16176      this.isLoading = false;
16177      this.loadedScripts.clear();
16178      this.loadedStyles.clear();
16179      // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected
16180      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16181    }
16182
16183    /**
16184     * Initialize the gallery system
16185     * @returns {Promise} Resolves when all core dependencies are loaded
16186     */
16187    initialize() {
16188      // Return existing promise if already initializing or complete
16189      if (this.initPromise) {
16190        return this.initPromise;
16191      }
16192
16193      console.log('EnhancedGallerySystem: Initializing gallery framework');
16194      this.isLoading = true;
16195
16196      // Create a promise to track loading completion
16197      this.initPromise = (async () => {
16198        try {
16199          // CRITICAL: Initialize neonGalleryState before anything else
16200          // This prevents "Cannot set properties of undefined" errors
16201          if (!window.neonGalleryState) {
16202            window.neonGalleryState = {
16203              initialized: false,
16204              initializing: false,
16205              domInitialized: false,
16206              configApplied: false,
16207              initializationPromise: null
16208            };
16209            console.log('EnhancedGallerySystem: Initialized neonGalleryState');
16210          }
16211
16212          // 1. First ensure all essential styles are loaded
16213          await this.loadStyles();
16214
16215          // 2. Preload all scripts to hint to the browser
16216          this.preloadScripts();
16217
16218          // 3. Ensure gallery container exists before scripts that require it
16219          this.ensureGalleryContainerPlaceholder(
16220            document.querySelector('.gallery-container')
16221          );
16222
16223          // 4. Load all essential scripts
16224          await this.loadScripts();
16225          
16226          // 5. Initialize the gallery events
16227          this.setupEvents();
16228          
16229          // Mark as fully initialized
16230          this.initialized = true;
16231          this.isLoading = false;
16232          
16233          console.log('EnhancedGallerySystem: Successfully initialized');
16234          return true;
16235        } catch (error) {
16236          console.error('EnhancedGallerySystem: Initialization failed:', error);
16237          this.isLoading = false;
16238          throw error;
16239        }
16240      })();
16241
16242      return this.initPromise;
16243    }
16244
16245    /**
16246     * Load all necessary stylesheet resources
16247     */
16248    async loadStyles() {
16249      console.log('EnhancedGallerySystem: Loading essential stylesheets');
16250      
16251      const stylePromises = this.essentialResources.styles.map(style => {
16252        // Skip if already loaded
16253        if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) {
16254          console.log(`Style already loaded: ${style.id}`);
16255          this.loadedStyles.add(style.id);
16256          return Promise.resolve();
16257        }
16258        
16259        return new Promise((resolve, reject) => {
16260          const link = document.createElement('link');
16261          link.id = style.id;
16262          link.rel = 'stylesheet';
16263          // Use proxy URL if flag is set, otherwise use CDN URL
16264          let styleUrl = `${style.href}?v=${this.version}`;
16265          if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) {
16266            styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version;
16267            console.log('🔍 Using proxy URL for style:', style.id, styleUrl);
16268          }
16269          
16270          link.href = styleUrl;
16271          
16272          link.onload = () => {
16273            console.log(`✅ Style loaded: ${style.id}`);
16274            this.loadedStyles.add(style.id);
16275            resolve();
16276          };
16277          
16278          link.onerror = (err) => {
16279            console.error(`❌ Failed to load style: ${style.id}`, err);
16280            if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) {
16281              console.error('🔄 Retrying via proxy:', style.href);
16282              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16283              link.href = proxyUrl + '?v=' + this.version;
16284              link.onerror = function() {
16285                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16286                link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
16287                // Don't reject - let it try storage
16288              };
16289            } else {
16290              reject(new Error(`Failed to load ${style.id}`));
16291            }
16292          };
16293          
16294          document.head.appendChild(link);
16295        });
16296      });
16297      
16298      // Wait for all styles to load
16299      await Promise.all(stylePromises);
16300      console.log('EnhancedGallerySystem: All stylesheets loaded');
16301    }
16302    
16303    /**
16304     * Preload scripts to improve browser loading priority
16305     */
16306    preloadScripts() {
16307      this.essentialResources.scripts.forEach(script => {
16308        // Enhanced check for already preloaded scripts
16309        const scriptName = script.src.split('/').pop();
16310        const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) ||
16311                                document.querySelector(`script[src*="${scriptName}"]`);
16312        
16313        if (alreadyPreloaded) {
16314          console.log(`Script already preloaded or loaded: ${script.id}`);
16315          return;
16316        }
16317        
16318        const preload = document.createElement('link');
16319        preload.rel = 'preload';
16320        preload.href = `${script.src}?v=${this.version}`;
16321        preload.as = 'script';
16322        document.head.appendChild(preload);
16323        
16324        console.log(`Script preloaded: ${script.id}`);
16325      });
16326    }
16327    
16328    /**
16329     * Load all necessary scripts
16330     */
16331    async loadScripts() {
16332      console.log('EnhancedGallerySystem: Loading essential scripts');
16333      
16334      // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils
16335      // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes.
16336      const loadOne = (script) => {
16337        const alreadyLoaded = this.loadedScripts.has(script.id) || 
16338                             document.getElementById(script.id) ||
16339                             document.querySelector(`script[src*="${script.src.split('/').pop()}"]`);
16340        
16341        if (alreadyLoaded) {
16342          const mainNeedsReload =
16343            script.id === 'neon-gallery-main' &&
16344            typeof window.neonGalleryUtils?.loadGallerySettings !== 'function';
16345          if (mainNeedsReload) {
16346            console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete — reloading`);
16347            document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16348              if (el.parentNode) el.parentNode.removeChild(el);
16349            });
16350            this.loadedScripts.delete(script.id);
16351            try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16352          } else {
16353          console.log(`Script already loaded: ${script.id}`);
16354          this.loadedScripts.add(script.id);
16355          return Promise.resolve();
16356          }
16357        }
16358        
16359        return new Promise((resolve, reject) => {
16360          const scriptElement = document.createElement('script');
16361          scriptElement.id = script.id;
16362          
16363          let scriptUrl = `${script.src}?v=${this.version}`;
16364          if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) {
16365            scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version;
16366            console.log('🔍 Using proxy URL for script:', script.id, scriptUrl);
16367          }
16368          
16369          scriptElement.src = scriptUrl;
16370          scriptElement.async = false;
16371          
16372          scriptElement.onload = () => {
16373            console.log(`✅ Script loaded: ${script.id}`);
16374            this.loadedScripts.add(script.id);
16375            resolve();
16376          };
16377          
16378          scriptElement.onerror = (err) => {
16379            console.error(`❌ Failed to load script: ${script.id}`, err);
16380            if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) {
16381              console.error('🔄 Retrying script via proxy:', script.id);
16382              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16383              scriptElement.src = proxyUrl + '?v=' + this.version;
16384              scriptElement.onerror = function() {
16385                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16386                scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
16387              };
16388            } else {
16389              reject(new Error(`Failed to load ${script.id}`));
16390            }
16391          };
16392          
16393          document.body.appendChild(scriptElement);
16394        });
16395      };
16396      
16397      for (const script of this.essentialResources.scripts) {
16398        await loadOne(script);
16399      }
16400      console.log('EnhancedGallerySystem: All scripts loaded');
16401      
16402      // Save references to original initialization functions after scripts are loaded
16403      if (window.initNeonGallery && typeof window.initNeonGallery === 'function') {
16404        this.originalInitFunctions.initNeonGallery = window.initNeonGallery;
16405      }
16406      
16407      if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') {
16408        this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave;
16409      }
16410    }
16411
16412    /**
16413     * Set up enhanced event listeners
16414     */
16415    setupEvents() {
16416      // Listen for gallery save events to capture latest state
16417      document.addEventListener('gallery-saved', (event) => {
16418        if (event.detail && event.detail.settings) {
16419          this.captureGalleryState(event.detail.settings);
16420        }
16421      });
16422      
16423      // Override existing functions to capture state changes
16424      this.enhanceExistingFunctions();
16425    }
16426    
16427    /**
16428     * Enhance existing gallery functions to work with our system
16429     */
16430    enhanceExistingFunctions() {
16431      // Save original functions
16432      const originalInit = window.initNeonGallery;
16433      const originalReinit = window.reinitializeGalleryAfterSave;
16434      
16435      // Only enhance if they exist
16436      if (typeof originalInit === 'function') {
16437        // Track if initNeonGallery is currently executing to prevent concurrent calls
16438        let isExecuting = false;
16439        let lastExecutionTime = 0;
16440        
16441        window.initNeonGallery = async (...args) => {
16442          // Prevent duplicate initializations within 1 second
16443          const now = Date.now();
16444          
16445          // Check if already executing or called recently
16446          if (isExecuting) {
16447            console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call');
16448            return Promise.resolve();
16449          }
16450          
16451          if (lastExecutionTime && (now - lastExecutionTime) < 1000) {
16452            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)');
16453            return Promise.resolve();
16454          }
16455          
16456          // Set flag and timestamp immediately to prevent concurrent calls
16457          isExecuting = true;
16458          lastExecutionTime = now;
16459          this.lastInitializationTime.set('initNeonGallery', now);
16460          
16461          try {
16462            // Call original with all arguments
16463            const result = await originalInit.apply(this, args);
16464            
16465            // Capture state after initialization
16466            if (window.neonGalleryConfig) {
16467              this.captureGalleryState(window.neonGalleryConfig);
16468            }
16469            
16470            return result;
16471          } finally {
16472            // Clear execution flag after a short delay to allow for async operations
16473            setTimeout(() => {
16474              isExecuting = false;
16475            }, 100);
16476          }
16477        };
16478      }
16479      
16480      if (typeof originalReinit === 'function') {
16481        window.reinitializeGalleryAfterSave = async (savedSettings) => {
16482          // Call original function
16483          const result = await originalReinit(savedSettings);
16484          
16485          // Capture updated state
16486          this.captureGalleryState(savedSettings);
16487          
16488          return result;
16489        };
16490      }
16491    }
16492    
16493    /**
16494     * Capture current gallery state for potential reuse
16495     */
16496    captureGalleryState(settings) {
16497      if (!settings) return;
16498      
16499      // Store a deep copy of the settings to avoid reference issues
16500      try {
16501        this.previousGalleryState = JSON.parse(JSON.stringify(settings));
16502        console.log('EnhancedGallerySystem: Captured gallery state for possible reuse');
16503      } catch (e) {
16504        console.error('EnhancedGallerySystem: Failed to capture gallery state:', e);
16505      }
16506    }
16507
16508    /**
16509     * Load a specific gallery using the enhanced system
16510     * @param {Object} gallery The gallery configuration
16511     * @param {HTMLElement} container The container to load the gallery into
16512     */
16513    async loadGallery(gallery, container, options) {
16514      if (!gallery) {
16515        console.error('EnhancedGallerySystem: Invalid gallery configuration');
16516        return false;
16517      }
16518
16519      const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
16520
16521      // Coalesce concurrent loads for the same gallery — await instead of wiping mid-flight
16522      if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) {
16523        const pageViewStillShowing = window._activeContentType === 'page' ||
16524          !!document.querySelector('.gallery-container .page-container, .page-container');
16525        if (!pageViewStillShowing) {
16526          console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id);
16527          return this._loadInProgress;
16528        }
16529        console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id);
16530        this._loadInProgress = null;
16531        this._loadingGalleryId = null;
16532      }
16533
16534      const now = Date.now();
16535      const lastInitTime = this.lastInitializationTime.get(gallery.id);
16536      if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) {
16537        const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry');
16538        const pageViewStillShowing = window._activeContentType === 'page' ||
16539          !!document.querySelector('.gallery-container .page-container, .page-container');
16540        if (hasContent && !pageViewStillShowing) {
16541          console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init');
16542          if (container) container.style.opacity = '1';
16543          if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
16544            window.syncGalleryOptionsButtonVisibility();
16545          }
16546          return true;
16547        }
16548      }
16549
16550      if (forceGalleryReload) {
16551        console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id);
16552        this.lastInitializationTime.delete(gallery.id);
16553        this._loadInProgress = null;
16554        this._loadingGalleryId = null;
16555      }
16556
16557      const loadTask = this._executeGalleryLoad(gallery, container, now, options);
16558      this._loadInProgress = loadTask;
16559      this._loadingGalleryId = gallery.id;
16560      try {
16561        return await loadTask;
16562      } finally {
16563        if (this._loadInProgress === loadTask) {
16564          this._loadInProgress = null;
16565          this._loadingGalleryId = null;
16566        }
16567      }
16568    }
16569
16570    async _executeGalleryLoad(gallery, container, startedAt, options) {
16571      this.lastInitializationTime.set(gallery.id, startedAt);
16572      const contentLoadGeneration = options && options.contentLoadGeneration != null
16573        ? options.contentLoadGeneration
16574        : window.__hydraContentLoadGeneration;
16575      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16576        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16577        : contentLoadGeneration === window.__hydraContentLoadGeneration &&
16578          (!window._activeContentType || window._activeContentType === 'gallery') &&
16579          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16580
16581      stopAutoAdvanceTimer();
16582
16583      // CRITICAL: Verify container exists
16584      if (!container) {
16585        console.error('EnhancedGallerySystem: Gallery container is null or undefined');
16586        return false;
16587      }
16588
16589  // Create a unique instance ID for this gallery load (needed before script init)
16590  const galleryInstanceId = Date.now();
16591
16592  try {
16593    // Clean up previous gallery state and prepare container BEFORE loading scripts
16594    this.cleanupGalleryState(container);
16595    const containerSetup = this.setupCorrectGalleryContainer(container);
16596    if (!containerSetup) {
16597      throw new Error('Failed to set up gallery container');
16598    }
16599    this.setupGalleryParameters(gallery, galleryInstanceId);
16600  } catch (prepError) {
16601    console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError);
16602    return false;
16603  }
16604
16605  // Make sure the system is initialized (scripts load with container already in DOM)
16606  try {
16607    await this.initialize();
16608  } catch (error) {
16609    console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error);
16610    //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.');
16611    return false;
16612  }
16613
16614  if (!stillCurrent()) {
16615    console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id);
16616    return false;
16617  }
16618
16619  if (!(await this.ensureNeonGalleryUtilsReady(container))) {
16620    console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load');
16621    return false;
16622  }
16623
16624  if (!stillCurrent()) {
16625    console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id);
16626    return false;
16627  }
16628
16629  // Update current gallery tracking
16630  this.currentGalleryId = gallery.id;
16631  
16632  console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`);
16633
16634  try {
16635    // Determine which initialization method to use
16636    if (this.canUseReinitForGallery(gallery, options)) {
16637      const result = await this.reinitializeGallery(gallery, container, options);
16638      return stillCurrent() ? result : false;
16639    } else {
16640      const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16641      return stillCurrent() ? result : false;
16642    }
16643  } catch (error) {
16644    console.error('EnhancedGallerySystem: Error loading gallery:', error);
16645    if (container) {
16646      console.error('EnhancedGallerySystem: Error loading gallery:', error);
16647      //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.');
16648    }
16649    return false;
16650  }
16651}
16652    
16653    normalizeGalleryIdentityValue(value) {
16654      if (value === undefined || value === null || value === '') return '';
16655      return String(value);
16656    }
16657
16658    previousGalleryStateBelongsTo(gallery, previousState) {
16659      if (!gallery || !previousState) return false;
16660
16661      const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id);
16662      const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId);
16663      if (!targetGalleryId && !targetPageId) return false;
16664
16665      const previousGalleryId = this.normalizeGalleryIdentityValue(
16666        previousState.galleryId || previousState.loadedGalleryId
16667      );
16668      const previousPageId = this.normalizeGalleryIdentityValue(
16669        previousState.pageId || previousState.initialPageUuid
16670      );
16671
16672      if (!previousGalleryId && !previousPageId) return false;
16673      if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) {
16674        return false;
16675      }
16676      if (previousPageId && targetPageId && previousPageId !== targetPageId) {
16677        return false;
16678      }
16679
16680      return (
16681        (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) ||
16682        (previousPageId && targetPageId && previousPageId === targetPageId)
16683      );
16684    }
16685
16686    /**
16687     * Determine if we can use reinitialization for this gallery
16688     */
16689    canUseReinitForGallery(gallery, options) {
16690      if (options?.forceReload === true || window._forceGalleryReload === true) {
16691        return false;
16692      }
16693      // Check if reinitializeGalleryAfterSave function exists
16694      if (typeof window.reinitializeGalleryAfterSave !== 'function') {
16695        return false;
16696      }
16697      
16698      // Check if we have a previous state to reuse
16699      if (!this.previousGalleryState) {
16700        return false;
16701      }
16702
16703      if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) {
16704        console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery');
16705        return false;
16706      }
16707      
16708      // Check if the gallery has the necessary properties for reinit
16709      if (!gallery.pageId || !gallery.galleryOptions) {
16710        return false;
16711      }
16712      
16713      // CRITICAL: Check if the gallery wrapper actually exists in the DOM
16714      // Reinitialization requires an existing gallery structure
16715      const galleryWrapper = document.querySelector('.gallery-wrapper');
16716      const neonContainer = document.getElementById('neon-gallery-container');
16717      if (!galleryWrapper || !neonContainer) {
16718        console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found');
16719        return false;
16720      }
16721      
16722      return true;
16723    }
16724    
16725    /**
16726     * Reinitialize a gallery using the reinitialization process
16727     */
16728async reinitializeGallery(gallery, container, options) {
16729  console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id);
16730  
16731  try {
16732    // CRITICAL: Ensure CSS is loaded before attempting reinitialization
16733    ensureGalleryStylesLoaded();
16734    
16735    // We should already have the proper container, but just in case:
16736    if (!document.getElementById('neon-gallery-container')) {
16737      this.setupCorrectGalleryContainer(container);
16738    }
16739    
16740    // CRITICAL: Ensure config is set up with all required fields before reinitialization
16741    const galleryOptions = gallery.galleryOptions || {};
16742    window.neonGalleryConfig = {
16743      useData: true,
16744      useCDN: true,
16745      version: 'live',
16746      manualCollectionName: galleryOptions.manualCollectionName || "mod",
16747      layoutType: galleryOptions.layoutType || "grid",
16748      ...galleryOptions,
16749      siteId: window.siteId || gallery.siteId || '',
16750      galleryId: gallery.id,
16751      // Explicitly include all permanent storage fields
16752      isPerma: galleryOptions.isPerma,
16753      permaURL: galleryOptions.permaURL,
16754      loadTxId: galleryOptions.loadTxId,
16755      loadPermaURL: galleryOptions.loadPermaURL,
16756      containerSelector: '#neon-gallery-container'
16757    };
16758    
16759    // Prepare settings by merging previous state with new gallery options.
16760    // Identity fields always come from the menu target, never leftover snapshot ids.
16761    const settings = {
16762      ...this.previousGalleryState,
16763      ...gallery.galleryOptions,
16764      pageId: gallery.pageId,
16765      initialPageUuid: gallery.pageId,
16766      galleryId: gallery.id,
16767      loadedGalleryId: gallery.id,
16768      forceReload: true,
16769      containerSelector: '#neon-gallery-container' // Add this explicitly
16770    };
16771    
16772    // Call reinitializeGalleryAfterSave with the merged settings
16773    window._reinitializeGalleryFromSystem = true;
16774    let result;
16775    try {
16776      result = await window.reinitializeGalleryAfterSave(settings);
16777    } finally {
16778      window._reinitializeGalleryFromSystem = false;
16779    }
16780    
16781    if (result) {
16782      console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully');
16783      // Fade in the container
16784      container.style.opacity = '1';
16785      return true;
16786    } else {
16787      throw new Error('Reinitialization did not complete successfully');
16788    }
16789  } catch (error) {
16790    console.error('EnhancedGallerySystem: Error reinitializing gallery:', error);
16791    // Fall back to standard initialization - ensure config is set up properly
16792    const galleryInstanceId = Date.now();
16793    // Re-setup parameters and config for the fallback
16794    this.setupGalleryParameters(gallery, galleryInstanceId);
16795    return this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16796  }
16797}
16798    
16799    /**
16800     * Initialize a gallery from scratch (fallback method)
16801     */
16802    async initializeNewGallery(gallery, container, galleryInstanceId, options) {
16803      console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id);
16804      const contentLoadGeneration = options && options.contentLoadGeneration != null
16805        ? options.contentLoadGeneration
16806        : window.__hydraContentLoadGeneration;
16807      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16808        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16809        : (!window._activeContentType || window._activeContentType === 'gallery') &&
16810          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16811      
16812      try {
16813        // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements
16814        ensureGalleryStylesLoaded();
16815        
16816        // Clean up container
16817        if (container) {
16818          container.innerHTML = '';
16819        } else {
16820          console.error('EnhancedGallerySystem: Container is null in initializeNewGallery');
16821          return false;
16822        }
16823        
16824        // Create gallery container with unique ID
16825        const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`;
16826        const neonGalleryContainer = document.createElement('div');
16827        neonGalleryContainer.id = 'neon-gallery-container';
16828        neonGalleryContainer.className = 'gallery-direct-content';
16829        neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
16830        neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString());
16831        neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId);
16832        neonGalleryContainer.style.width = '100%';
16833        neonGalleryContainer.style.height = '100%';
16834        container.appendChild(neonGalleryContainer);
16835        
16836        // CRITICAL: Initialize neonGalleryState if it doesn't exist
16837        if (!window.neonGalleryState) {
16838          window.neonGalleryState = {
16839            initialized: false,
16840            initializing: false,
16841            domInitialized: false,
16842            configApplied: false,
16843            initializationPromise: null
16844          };
16845        }
16846
16847        // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields
16848        // Don't rely on conditional logic - always set it correctly here
16849        const galleryOptions = gallery.galleryOptions || {};
16850        window.neonGalleryConfig = {
16851          useData: true,
16852          useCDN: true,
16853          version: 'live',
16854          manualCollectionName: galleryOptions.manualCollectionName || "mod",
16855          layoutType: galleryOptions.layoutType || "grid",
16856          ...galleryOptions,
16857          siteId: window.siteId || gallery.siteId || '',
16858          galleryId: gallery.id,
16859          galleryInstanceId: galleryInstanceId,
16860          containerSelector: '#neon-gallery-container',
16861          // Explicitly include all permanent storage fields (critical for proper initialization)
16862          isPerma: galleryOptions.isPerma,
16863          permaURL: galleryOptions.permaURL,
16864          loadTxId: galleryOptions.loadTxId,
16865          loadPermaURL: galleryOptions.loadPermaURL
16866        };
16867        
16868        // Call the initialization function directly if available
16869        if (typeof window.initNeonGallery === 'function') {
16870          console.log('EnhancedGallerySystem: Calling direct initialization method');
16871          
16872          // Additional check: prevent duplicate initNeonGallery calls within 1 second
16873          const initKey = 'init_' + gallery.id + '_' + galleryInstanceId;
16874          const lastInitCall = this.lastInitializationTime.get(initKey);
16875          const now = Date.now();
16876          if (lastInitCall && (now - lastInitCall) < 1000) {
16877            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + ' 
16877(last call ' + (now - lastInitCall) + 'ms ago)');
16878            container.style.opacity = '1';
16879            return true;
16880          }
16881          this.lastInitializationTime.set(initKey, now);
16882          
16883          try {
16884            // Wait a tiny bit to ensure DOM updates have propagated
16885            await new Promise(resolve => setTimeout(resolve, 10));
16886            if (!stillCurrent()) {
16887              console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery');
16888              return false;
16889            }
16890            
16891            const result = await window.initNeonGallery();
16892            if (!stillCurrent()) {
16893              console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion');
16894              return false;
16895            }
16896            console.log('EnhancedGallerySystem: Direct initialization successful');
16897            
16898            // Save state for future loads
16899            if (window.neonGalleryConfig) {
16900              this.captureGalleryState(window.neonGalleryConfig);
16901            }
16902            
16903            // Fade in the container
16904            container.style.opacity = '1';
16905            return true;
16906          } catch (e) {
16907            if (!stillCurrent()) {
16908              console.log('EnhancedGallerySystem: Ignoring stale direct initialization error');
16909              return false;
16910            }
16911            console.error('EnhancedGallerySystem: Direct initialization failed:', e);
16912            throw e;
16913          }
16914        } else {
16915          // If initNeonGallery isn't available, use event-based initialization
16916          console.log('EnhancedGallerySystem: Using event-based initialization fallback');
16917          
16918          // Create an event to trigger initialization
16919          const initEvent = new CustomEvent('neon-gallery-init', { 
16920            detail: { 
16921              galleryId: gallery.id,
16922              instanceId: galleryInstanceId,
16923              containerId: galleryContainerId
16924            }
16925          });
16926          document.dispatchEvent(initEvent);
16927          
16928          // Wait a short time to see if initialization completes
16929          await new Promise(resolve => setTimeout(resolve, 500));
16930          if (!stillCurrent()) {
16931            console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization');
16932            return false;
16933          }
16934          
16935          // Fade in regardless of result - gallery script should handle displaying errors
16936          container.style.opacity = '1';
16937          return true;
16938        }
16939      } catch (error) {
16940        console.error('EnhancedGallerySystem: Error in standard initialization:', error);
16941        if (!stillCurrent()) {
16942          console.log('EnhancedGallerySystem: Ignoring stale standard initialization error');
16943          return false;
16944        }
16945        if (container) {
16946          this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.');
16947        }
16948        return false;
16949      }
16950    }
16951
16952    /**
16953     * Clean up gallery state without removing loaded scripts
16954     */
16955    cleanupGalleryState(container) {
16956      console.log('EnhancedGallerySystem: Cleaning up gallery state');
16957      
16958      // Stop any ongoing singles autoplay from the previous gallery
16959      if (typeof window.stopSinglesAutoplay === 'function') {
16960        try {
16961          window.stopSinglesAutoplay(); 
16962          console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.');
16963        } catch (error) {
16964          console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error);
16965        }
16966      } else {
16967        console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.');
16968      }
16969
16970      // 1. Reset global gallery state variables
16971      const resetGlobals = [
16972        'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
16973        'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 
16974        'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 
16975        'neonGalleryInstance', 'currentGalleryId', 'galleryData', 
16976        'imageCache', 'thumbnailCache'
16977      ];
16978      
16979      resetGlobals.forEach(prop => {
16980        if (window[prop] !== undefined) {
16981          window[prop] = null;
16982        }
16983      });
16984      
16985      // 2. Reset neonGalleryState if it's a proper object with properties
16986      if (window.neonGalleryState && typeof window.neonGalleryState === 'object') {
16987        window.neonGalleryState = {
16988          initialized: false,
16989          initializing: false,
16990          domInitialized: false,
16991          configApplied: false,
16992          initializationPromise: null
16993        };
16994      }
16995      
16996      // 3. Remove lightbox elements and overlays
16997      document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => {
16998        if (el && el.parentNode) {
16999          el.parentNode.removeChild(el);
17000        }
17001      });
17002
17003      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17004      if (staleOptionsOverlay && staleOptionsOverlay.parentNode) {
17005        staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay);
17006      }
17007      window.optionsVisible = false;
17008      
17009      // 4. Remove any gallery-specific DOM elements
17010      document.querySelectorAll(
17011        '.neon-gallery-wrapper, .fullscreen-overlay, ' + 
17012        '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' +
17013        '.neon-thumbnails, .neon-image, .neon-caption, ' +
17014        '.neon-controls, .neon-pagination'
17015      ).forEach(el => {
17016        if (el && el.parentNode) {
17017          el.parentNode.removeChild(el);
17018        }
17019      });
17020      
17021      // 5. Clear container if provided and valid
17022      if (container) {
17023        container.innerHTML = '';
17024        container.style.opacity = '0';
17025      }
17026      
17027      // 6. Clear keyboard listeners to prevent multiple listeners from different galleries
17028      if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
17029        window.removeAllLightboxKeyboardListeners();
17030        console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup');
17031      } else if (window.currentLightboxKeyboardListener) {
17032        document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
17033        window.currentLightboxKeyboardListener = null;
17034        console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup');
17035      }
17036      window.lightboxKeyboardListenerActive = false;
17037    }
17038    
17039    /**
17040     * Prepare container for gallery loading
17041     */
17042    prepareContainer(container, gallery, instanceId) {
17043      // CRITICAL: Add null check for container
17044      if (!container) {
17045        console.error('EnhancedGallerySystem: Container is null in prepareContainer');
17046        return;
17047      }
17048      
17049      try {
17050        // Add attributes and data
17051        container.setAttribute('data-gallery-id', gallery.id.toString());
17052        container.setAttribute('data-instance-id', instanceId.toString());
17053        
17054        // Add loading indicator
17055        const loadingIndicator = document.createElement('div');
17056        loadingIndicator.className = 'gallery-loading';
17057        loadingIndicator.innerHTML = `
17058          <div class="spinner"></div>
17059          <div class="loading-text">Loading Gallery...</div>
17060        `;
17061        
17062        container.appendChild(loadingIndicator);
17063      } catch (error) {
17064        console.error('EnhancedGallerySystem: Error preparing container:', error);
17065      }
17066    }
17067    
17068
17069
17070
17071setupGalleryParameters(gallery, galleryInstanceId) {
17072  // Set up gallery parameters with enhanced uniqueness
17073  window.Parameters = window.Parameters || {};
17074  
17075  // Clone and extend existing Parameters to avoid losing important values
17076  const existingParams = { ...window.Parameters };
17077  
17078  window.Parameters = {
17079    ...existingParams,
17080    SiteAlias: window.location.hostname,
17081    InitialPageUuid: gallery.pageId,
17082    InitialPageAlias: gallery.slug || this.slugify(gallery.title),
17083    // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode
17084    // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active
17085    isInEditor: window.isEditing || 
17086                localStorage.getItem('hydra_is_admin') === 'true' || 
17087                window.isAdmin === true || 
17088                document.body.classList.contains('hydra-admin'),
17089    siteId: window.siteId || gallery.siteId || '',
17090    isHydra: true,
17091    
17092    // CRITICAL: These properties help the gallery script find the container
17093    galleryContainerId: 'neon-gallery-container',
17094    containerSelector: '#neon-gallery-container',
17095    
17096    // Additional properties
17097    galleryInstanceId: galleryInstanceId,
17098    galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`,
17099    loadTimestamp: galleryInstanceId,
17100    loadedGalleryId: gallery.id
17101  };
17102
17103  // Set up gallery config - CRITICAL: Include all permanent storage fields
17104  const galleryOptions = gallery.galleryOptions || {};
17105  window.neonGalleryConfig = {
17106    useData: true,
17107    useCDN: true,
17108    version: 'live',
17109    manualCollectionName: galleryOptions.manualCollectionName || "mod",
17110    layoutType: galleryOptions.layoutType || "grid",
17111    ...galleryOptions,
17112    siteId: window.siteId || gallery.siteId || '',
17113    galleryId: gallery.id,
17114    galleryInstanceId: galleryInstanceId,
17115    
17116    // Add container selector here too
17117    containerSelector: '#neon-gallery-container',
17118    
17119    // CRITICAL: Explicitly include all permanent storage fields to ensure they're available
17120    // These are needed for proper gallery initialization and data loading
17121    isPerma: galleryOptions.isPerma,
17122    permaURL: galleryOptions.permaURL,
17123    loadTxId: galleryOptions.loadTxId,
17124    loadPermaURL: galleryOptions.loadPermaURL
17125  };
17126}
17127    
17128    /**
17129     * Show error message in container
17130     */
17131    showErrorMessage(container, message) {
17132      // CRITICAL: Add null check for container
17133      if (!container) {
17134        console.error('EnhancedGallerySystem: Container is null in showErrorMessage');
17135        return;
17136      }
17137      
17138      try {
17139        // Clear container
17140        container.innerHTML = '';
17141        
17142        // Create error message element
17143        const errorElement = document.createElement('div');
17144        errorElement.className = 'gallery-error';
17145        errorElement.innerHTML = `
17146          <h3>Error Loading Gallery</h3>
17147          <p>${message}</p>
17148        `;
17149        container.appendChild(errorElement);
17150        container.style.opacity = '1';
17151      } catch (error) {
17152        console.error('EnhancedGallerySystem: Error showing error message:', error);
17153      }
17154    }
17155    
17156    /**
17157     * Utility function to convert title to slug
17158     */
17159    slugify(text) {
17160      return text
17161        .toLowerCase()
17162        .replace(/s+/g, '-')
17163        .replace(/[^w-]+/g, '')
17164        .replace(/--+/g, '-')
17165        .trim();
17166    }
17167  }
17168
17169  // Create a singleton instance
17170  window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem();
17171
17172  /**
17173   * Enhanced loadGallery function using the EnhancedGallerySystem
17174   * This replaces the existing loadGallery function
17175   */
17176const enhancedLoadGallery = async function(id, event, options) {
17177    const originalLoadGallery = window.originalLoadGallery;
17178    const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
17179
17180    stopAutoAdvanceTimer();
17181    if (typeof dismissGalleryPasswordOverlay === 'function') {
17182      dismissGalleryPasswordOverlay();
17183    }
17184
17185    if (event && event.target && event.target.isConnected === false) {
17186      console.log('enhancedLoadGallery: Ignoring navigation from detached element');
17187      return;
17188    }
17189
17190    if (event) {
17191      event.preventDefault();
17192      event.stopPropagation();
17193    }
17194
17195    let galleryContainer = document.querySelector('.gallery-container');
17196
17197    if (!galleryContainer) {
17198      galleryContainer = document.getElementById('galleryFrame')?.parentElement ||
17199                        document.getElementById('main-content') ||
17200                        document.getElementById('content');
17201
17202      if (!galleryContainer) {
17203        console.error('Gallery container not found, falling back to original function');
17204        if (typeof originalLoadGallery === 'function') {
17205          return originalLoadGallery(id, event, options);
17206        } else {
17207          console.error('Gallery container not found and no fallback available');
17208          return;
17209        }
17210      }
17211    }
17212
17213    // Bail out before hiding/clearing when the same gallery is already loading or rendered.
17214    // Post-save passes { forceReload: true } to bypass this path without affecting navigation.
17215    const egEarly = window.EnhancedGallerySystem;
17216    if (egEarly && !forceGalleryReload) {
17217      if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) {
17218        const pageViewStillShowing = window._activeContentType === 'page' ||
17219          !!document.querySelector('.gallery-container .page-container, .page-container');
17220        if (pageViewStillShowing) {
17221          console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id);
17222          egEarly._loadInProgress = null;
17223          egEarly._loadingGalleryId = null;
17224        } else {
17225          console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id);
17226          await egEarly._loadInProgress;
17227          if (galleryContainer) galleryContainer.style.opacity = '1';
17228          return;
17229        }
17230      }
17231      const hasRenderedGallery = !!document.querySelector(
17232        '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'
17233      );
17234      // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view.
17235      const pageViewStillShowing = window._activeContentType === 'page' ||
17236        !!document.querySelector('.gallery-container .page-container, .page-container');
17237      if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) {
17238        console.log('enhancedLoadGallery: gallery already rendered, skipping reload');
17239        if (galleryContainer) galleryContainer.style.opacity = '1';
17240        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17241          window.syncGalleryOptionsButtonVisibility();
17242        }
17243        return;
17244      }
17245    } else if (forceGalleryReload) {
17246      console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip');
17247      if (egEarly?.lastInitializationTime?.delete) {
17248        egEarly.lastInitializationTime.delete(id);
17249      }
17250      if (egEarly) {
17251        egEarly._loadInProgress = null;
17252        egEarly._loadingGalleryId = null;
17253      }
17254    }
17255
17256    if (galleryContainer) {
17257      galleryContainer.style.opacity = '0';
17258    }
17259
17260    console.log('Loading gallery with ID (enhancedLoadGallery):', id);
17261
17262    const gallery = findGalleryById(galleries, id);
17263
17264    if (!gallery) {
17265      console.warn('No gallery found with ID:', id);
17266      if (galleryContainer) {
17267        galleryContainer.innerHTML = `
17268          <div class="gallery-error">
17269            <h3>Gallery Not Found</h3>
17270            <p>The requested gallery could not be found.</p>
17271          </div>
17272        `;
17273        galleryContainer.style.opacity = '1';
17274      }
17275      return;
17276    }
17277
17278    if (isPageMenuItem(gallery)) {
17279      return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event);
17280    }
17281
17282    window._activeContentType = 'gallery';
17283
17284    if (gallery.isSubmenu) {
17285      console.log('Gallery is submenu, skipping URL update');
17286      toggleSubmenu(id, event || { stopPropagation: () => {} });
17287      if (galleryContainer){
17288      galleryContainer.style.opacity = '1';
17289      }
17290      return;
17291    }
17292
17293    const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
17294    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
17295      window.PageManager.clearCurrentPage();
17296    }
17297
17298    console.log('Found gallery:', gallery.title);
17299
17300    // Check for gallery-level password protection (only for visitors, not in edit mode)
17301    if (!isInEditMode() && gallery.passwordProtected) {
17302      // Use PageManager's gallery password protection methods
17303      if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 
17304          typeof window.PageManager.createGalleryPasswordOverlay === 'function') {
17305        
17306        if (!window.PageManager.checkGalleryPasswordAccess(gallery)) {
17307          console.log('Gallery is password protected, showing overlay');
17308          window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => {
17309            if (accessGranted) {
17310              // Reload the gallery now that access is granted
17311              enhancedLoadGallery(id, event);
17312            }
17313          });
17314          return; // Exit early, will reload after password is entered
17315        }
17316      }
17317    }
17318
17319    window.activeGalleryId = id;
17320    activeGalleryId = id;
17321
17322    // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery.
17323    // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(),
17324    // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original
17325    // loadGallery) and clear the container so page content does not block gallery rendering.
17326    if (typeof window.removeGalleryScriptsWithPause === 'function') {
17327      await window.removeGalleryScriptsWithPause();
17328    }
17329    if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17330      console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id);
17331      return;
17332    }
17333    if (galleryContainer) {
17334      galleryContainer.innerHTML = '';
17335    }
17336
17337    const bodyEl = document.body;
17338    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
17339        bodyEl.classList.add('menu-hidden-on-page');
17340    } else {
17341        bodyEl.classList.remove('menu-hidden-on-page');
17342    }
17343    applyMenuOverlayClassForGallery(gallery);
17344
17345    try {
17346      if (typeof updateActiveStates === 'function') updateActiveStates();
17347      if (typeof updateMobileTitle === 'function') updateMobileTitle();
17348      if (typeof closeMobileMenu === 'function') closeMobileMenu();
17349      // Close the gallery options panel when navigating to a different gallery
17350      if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) {
17351        window.closeOptionsPanel();
17352      }
17353      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17354      if (staleOptionsOverlay) {
17355        staleOptionsOverlay.remove();
17356      }
17357      window.optionsVisible = false;
17358      if (typeof updateURLWithGallerySlug === 'function') {
17359        updateURLWithGallerySlug(gallery);
17360      }
17361    } catch (uiError) {
17362      console.error('Error updating UI state:', uiError);
17363    }
17364
17365    try {
17366      // Add detailed logging about EnhancedGallerySystem availability
17367      console.log('🔍 Checking EnhancedGallerySystem availability:', {
17368        exists: !!window.EnhancedGallerySystem,
17369        hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17370        initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized),
17371        isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading)
17372      });
17373      
17374      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') {
17375        console.log('✅ EnhancedGallerySystem available, calling loadGallery');
17376        const result = await window.EnhancedGallerySystem.loadGallery(
17377          gallery,
17378          galleryContainer,
17379          {
17380            ...(options || {}),
17381            ...(forceGalleryReload ? { forceReload: true } : {}),
17382            contentLoadGeneration
17383          }
17384        );
17385        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17386          console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id);
17387          return;
17388        }
17389        console.log('📊 EnhancedGallerySystem.loadGallery result:', result);
17390
17391        if (!result && typeof originalLoadGallery === 'function') {
17392          console.log('⚠️ Enhanced gallery loading failed, falling back to original function');
17393          return originalLoadGallery(id, event, options);
17394        }
17395        if (galleryContainer) {
17396          galleryContainer.style.opacity = '1';
17397        }
17398        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17399          window.syncGalleryOptionsButtonVisibility();
17400          setTimeout(window.syncGalleryOptionsButtonVisibility, 350);
17401        }
17402      } else {
17403        console.error('❌ EnhancedGallerySystem or its loadGallery method is not available.', {
17404          EnhancedGallerySystem: !!window.EnhancedGallerySystem,
17405          loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17406          useCdnProxy: window.useCdnProxy || false
17407        });
17408        
17409        // Fallback: Try using createGalleryContext directly
17410        console.log('🔄 Attempting fallback: createGalleryContext');
17411        if (typeof createGalleryContext === 'function') {
17412          createGalleryContext(gallery, galleryContainer);
17413          if (galleryContainer) {
17414            galleryContainer.style.opacity = '1';
17415          }
17416          return;
17417        }
17418        
17419        if (galleryContainer) {
17420            galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`;
17421            galleryContainer.style.opacity = '1';
17422        }
17423        return;
17424      }
17425    } catch (error) {
17426      console.error('Error loading gallery:', error);
17427      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17428        console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id);
17429        return;
17430      }
17431      if (galleryContainer) {
17432        try {
17433          galleryContainer.innerHTML = `
17434            <div class="gallery-error">
17435              <h3>Error Loading Gallery</h3>
17436              <p>There was a problem loading the gallery. Please try again later.</p>
17437              <p class="error-details">${error.message}</p>
17438            </div>
17439          `;
17440          galleryContainer.style.opacity = '1';
17441        } catch (containerError) {
17442          console.error('Error showing error message in container:', containerError);
17443        }
17444      }
17445      if (typeof originalLoadGallery === 'function') {
17446        console.log('Falling back to original loadGallery after error');
17447        return originalLoadGallery(id, event, options);
17448      }
17449    }
17450  };
17451
17452  /**
17453   * Initialization function to set up the gallery system
17454   */
17455  function initializeGallerySystem() {
17456    console.log('Initializing enhanced gallery system');
17457    
17458    // Store reference to original function before replacing
17459    if (typeof window.loadGallery === 'function') {
17460      window.originalLoadGallery = window.loadGallery;
17461    }
17462    
17463    // Replace the existing loadGallery function with our optimized version
17464    window.loadGallery = enhancedLoadGallery;
17465    
17466    // Add this CSS for loading/error states
17467    addGallerySystemStyles();
17468    
17469    // Preload styles only at boot — scripts load when first gallery opens (with container ready)
17470    window.EnhancedGallerySystem.preloadStylesOnly().catch(error => {
17471      console.warn('Gallery styles preloading failed:', error);
17472    });
17473  }
17474
17475  /**
17476   * Add CSS styles for loading indicators and error messages
17477   */
17478  function addGallerySystemStyles() {
17479    const styleId = 'enhanced-gallery-system-styles';
17480    
17481    // Skip if already added
17482    if (document.getElementById(styleId)) {
17483      return;
17484    }
17485    
17486    const style = document.createElement('style');
17487    style.id = styleId;
17488    style.textContent = `
17489      /* Loading indicator */
17490      .gallery-loading {
17491        display: flex;
17492        flex-direction: column;
17493        justify-content: center;
17494        align-items: center;
17495        width: 100%;
17496        height: 100%;
17497        min-height: 200px;
17498        opacity: 0 !important;
17499      }
17500      
17501      .gallery-loading .spinner {
17502        width: 50px;
17503        height: 50px;
17504        border: 3px solid rgba(0, 0, 0, 0.1);
17505        border-radius: 50%;
17506        border-top-color: #4682B4;
17507        animation: spin 1s ease-in-out infinite;
17508        margin-bottom: 20px;
17509        opacity: 0 !important;
17510      }
17511      
17512      @keyframes spin {
17513        to { transform: rotate(360deg); }
17514      }
17515      
17516      .gallery-loading .loading-text {
17517        font-family: Arial, sans-serif;
17518        font-size: 16px;
17519        color: #555;
17520        opacity: 0;
17521      }
17522      
17523      /* Error message */
17524      .gallery-error {
17525        display: flex;
17526        flex-direction: column;
17527        justify-content: center;
17528        align-items: center;
17529        width: 100%;
17530        height: 100%;
17531        min-height: 200px;
17532        padding: 30px;
17533        text-align: center;
17534      }
17535      
17536      .gallery-error h3 {
17537        font-family: Arial, sans-serif;
17538        font-size: 24px;
17539        color:rgba(211, 47, 47, 0.25);
17540        margin-bottom: 20px;
17541      }
17542      
17543      .gallery-error p {
17544        font-family: Arial, sans-serif;
17545        font-size: 16px;
17546        color: #555;
17547        max-width: 600px;
17548        margin-bottom: 10px;
17549      }
17550      
17551      .gallery-error .error-details {
17552        font-size: 14px;
17553        color: #888;
17554        font-style: italic;
17555      }
17556    `;
17557    
17558    document.head.appendChild(style);
17559  }
17560
17561  // Initialize gallery system immediately if possible
17562  initializeGallerySystem();
17563})();
17564
17565
17566/**
17567 * Enhanced Gallery Link Interceptor
17568 * Handles clicks on gallery links to prevent full page reloads
17569 */
17570(function() {
17571  // Store any loaded gallery data for quick reference
17572  const GALLERY_DATA_CACHE = {
17573    initialized: false,
17574    galleries: [],
17575    lastUpdate: 0
17576  };
17577
17578  // Initialize when DOM is ready to catch early clicks
17579  document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor);
17580  
17581  // Also initialize when window loads to ensure complete gallery data
17582  window.addEventListener('load', function() {
17583    // Use a small delay to ensure galleries have fully initialized
17584    setTimeout(initGalleryLinkInterceptor, 200);
17585  });
17586  
17587  // Additional init on first user interaction
17588  document.addEventListener('click', function initOnFirstClick() {
17589    initGalleryLinkInterceptor();
17590    // Remove this listener after first click
17591    document.removeEventListener('click', initOnFirstClick);
17592  }, { once: true });
17593  
17594  // Main initialization function
17595  function initGalleryLinkInterceptor() {
17596    // Prevent multiple initializations
17597    if (window._galleryLinkInterceptorInitialized) {
17598      // If already initialized, just refresh the gallery data
17599      refreshGalleryCache();
17600      return;
17601    }
17602    
17603    window._galleryLinkInterceptorInitialized = true;
17604    console.log('Initializing enhanced gallery link interceptor');
17605    
17606    // Update our cache with current galleries data
17607    refreshGalleryCache();
17608    
17609    // Get primary containers that might contain links
17610    const galleryContainer = document.querySelector('.gallery-container');
17611    const mainContainer = document.querySelector('.container') || document.body;
17612    
17613    if (!galleryContainer && !mainContainer) {
17614      console.warn('No gallery containers found, link interceptor partially initialized');
17615    }
17616    
17617    // Add global click handler to catch all link clicks
17618    document.addEventListener('click', handleGlobalLinkClick, true);
17619    
17620    // Try to intercept links from iframes - critical for gallery content
17621    interceptAllIframeLinks();
17622    
17623    // Set up a mutation observer to handle dynamically added content
17624    setupMutationObserver();
17625    
17626    // Log initialization with site details
17627    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17628    console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`);
17629    
17630    // Create global helper for debugging
17631    window.debugGalleryLinks = function() {
17632      return {
17633        inPreviewMode: isPreviewMode,
17634        interceptorInitialized: window._galleryLinkInterceptorInitialized,
17635        cachedGalleries: GALLERY_DATA_CACHE.galleries.length,
17636        urlPath: window.location.pathname,
17637        siteGuid: getSiteGuidFromPath()
17638      };
17639    };
17640  }
17641  
17642  // Update the gallery cache when gallery data changes
17643function refreshGalleryCache() {
17644  // Initialize GALLERY_DATA_CACHE if it doesn't exist
17645  if (!window.GALLERY_DATA_CACHE) {
17646    window.GALLERY_DATA_CACHE = {
17647      initialized: false,
17648      galleries: [],
17649      lastUpdate: 0
17650    };
17651  }
17652  
17653  // Check for global galleries variable first
17654  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
17655    // Use the global galleries array
17656    window.GALLERY_DATA_CACHE.galleries = galleries;
17657    window.GALLERY_DATA_CACHE.initialized = true;
17658    window.GALLERY_DATA_CACHE.lastUpdate = Date.now();
17659    window.galleries = galleries;
17660
17661    console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`);
17662    return;
17663  }
17664    
17665    // Debug log
17666    console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`);
17667  }
17668  
17669  // Global click handler - catches all link clicks
17670  function handleGlobalLinkClick(e) {
17671    // Find if a link was clicked
17672    const link = e.target.closest('a');
17673    if (!link || !link.href) return;
17674    
17675    // Skip if the link uses special targets or modifiers
17676    if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17677    
17678    // Skip for hash links or javascript: links
17679    if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17680    
17681    try {
17682      // Parse the URL to work with it
17683      const url = new URL(link.href);
17684      
17685      // Only intercept links to the same domain
17686      if (url.hostname !== window.location.hostname) return;
17687      
17688      // Skip API and resource URLs
17689      if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return;
17690      
17691      console.log('Link intercepted:', url.pathname);
17692      
17693      // For preview URLs, we need to handle the path specially
17694      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17695      const siteGuid = getSiteGuidFromPath();
17696      
17697      console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid);
17698
17699      // Site Graph card links carry data-sg-target-id — use SPA navigation (not preview iframe fallback)
17700      if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) {
17701        let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries))
17702          ? galleries
17703          : window.galleries;
17704        const sgItem = Array.isArray(galleriesData)
17705          ? findGalleryById(galleriesData, link.dataset.sgTargetId)
17706          : null;
17707        if (sgItem) {
17708          e.preventDefault();
17709          e.stopPropagation();
17710          if (sgItem.isPage && window.loadPage) {
17711            window.loadPage(sgItem.id);
17712          } else if (window.loadGallery) {
17713            window.loadGallery(sgItem.id);
17714          }
17715          let targetUrl = url.href;
17716          if (isPreviewMode && siteGuid) {
17717            const pathname = url.pathname.startsWith(`/${siteGuid}/`) ?
17718              url.pathname :
17719              `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17720            const newUrl = new URL(url);
17721            newUrl.pathname = pathname;
17722            targetUrl = newUrl.href;
17723          }
17724          window.history.pushState(
17725            { galleryId: sgItem.id },
17726            sgItem.title || document.title,
17727            targetUrl
17728          );
17729          document.title = formatGalleryDocumentTitle(sgItem.title);
17730          window.scrollTo(0, 0);
17731          return;
17732        }
17733      }
17734      
17735      // Check if this is internal navigation first
17736      let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17737      
17738      console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none');
17739      
17740      if (matchingItem) {
17741        // Prevent default navigation (full page reload)
17742        e.preventDefault();
17743        e.stopPropagation();
17744        
17745        console.log('Found matching gallery for link:', matchingItem.title);
17746
17747        const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function'
17748          ? window.isNestedGalleryLinkModeEnabled()
17749          : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes);
17750        const isGridGalleryLink = !!(link.closest('.masonry-item') ||
17751          (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') &&
17752           link.querySelector('img') &&
17753           !link.closest('#neon-lightbox, .neon-lightbox')));
17754
17755        if (!matchingItem.isPage &&
17756            nestedLinkModeActive &&
17757            isGridGalleryLink &&
17758            typeof window.openNestedGalleryLightbox === 'function') {
17759          void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => {
17760            if (!opened) {
17761              console.warn('Nested gallery lightbox failed — staying on parent gallery (no loadGallery fallback)');
17762            }
17763          });
17764          return;
17765        }
17766        
17767        // Use the existing navigation functions
17768        if (matchingItem.isPage && window.loadPage) {
17769          window.loadPage(matchingItem.id);
17770        } else if (window.loadGallery) {
17771          window.loadGallery(matchingItem.id);
17772        }
17773        
17774        // Construct proper URL for preview mode
17775        let targetUrl = url.href;
17776        if (isPreviewMode && siteGuid) {
17777          // Ensure we keep the GUID in the URL path
17778          const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17779            url.pathname : 
17780            `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17781          
17782          const newUrl = new URL(url);
17783          newUrl.pathname = pathname;
17784          targetUrl = newUrl.href;
17785        }
17786        
17787        // Update URL without reload
17788        window.history.pushState(
17789          { galleryId: matchingItem.id },
17790          matchingItem.title || document.title,
17791          targetUrl
17792        );
17793        
17794        // Update page title
17795        document.title = formatGalleryDocumentTitle(matchingItem.title);
17796        
17797        // Scroll to top
17798        window.scrollTo(0, 0);
17799        
17800        return;
17801      } 
17802      // Special case for preview mode - even if no matching gallery found
17803      else if (isPreviewMode && siteGuid) {
17804        // We're in preview mode with a GUID - intercept the navigation
17805        // to maintain the GUID in the URL. Only do this when #galleryFrame
17806        // can load content; otherwise allow SPA onclick handlers to run.
17807        
17808        console.log('No direct match but in preview mode - handling navigation');
17809
17810        const frame = document.getElementById('galleryFrame');
17811
17812        if (!frame) {
17813          console.warn('Gallery frame not found - allowing default/SPA handlers');
17814        } else {
17815          // Prevent default navigation
17816          e.preventDefault();
17817          e.stopPropagation();
17818          
17819          // Construct a URL that preserves the GUID
17820          let newPathname = url.pathname;
17821          if (!newPathname.startsWith(`/${siteGuid}/`)) {
17822            // Add the GUID to the path if not already there
17823            newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17824          }
17825          
17826          // Create the preview-friendly URL for browser history
17827          const browserUrl = new URL(window.location.origin);
17828          browserUrl.pathname = newPathname;
17829          
17830          console.log('Constructing preview URL with GUID:', browserUrl.href);
17831          
17832          // IMPORTANT: For gallery navigation, we need to create a special URL
17833          // that tells the gallery system to load the content directly
17834          console.log('Loading preview path in iframe:', newPathname);
17835          
17836          // First update browser URL to show the correct path
17837          window.history.pushState(
17838            { customPath: newPathname },
17839            document.title,
17840            browserUrl.href
17841          );
17842          
17843          // Create direct path to the gallery content
17844          const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
17845          
17846          // For direct gallery paths, we need to load the gallery with special parameters
17847          // This tells the gallery system to load content from this path
17848          const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`;
17849          
17850          // Two approaches to loading the content:
17851          
17852          // 1. Use the gallery wrapper approach
17853          const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
17854            `?siteId=${siteGuid}` +
17855            `&path=${encodeURIComponent(cleanPath)}` + 
17856            `&directPath=true` +
17857            `&t=${Date.now()}`; // Cache buster
17858          
17859          console.log('Loading gallery via wrapper URL:', wrapperUrl);
17860          frame.src = wrapperUrl;
17861          
17862          // Clear any active gallery ID since we're navigating directly
17863          if (window.activeGalleryId) {
17864            console.log('Clearing active gallery ID during direct navigation');
17865            window.activeGalleryId = null;
17866          }
17867          
17868          return; // Critical: prevent further navigation
17869        }
17870      }
17871      
17872      console.log('No matching gallery or special handling - allow normal navigation');
17873    } catch (error) {
17874      console.error('Error processing link click:', error);
17875    }
17876  }
17877  
17878  // Handle all iframes on the page
17879  function interceptAllIframeLinks() {
17880    // Look for any iframe that might contain gallery content
17881    const iframes = document.querySelectorAll('iframe');
17882    
17883    iframes.forEach(iframe => {
17884      if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) {
17885        // Primary gallery iframe - high priority
17886        setupIframeIntercept(iframe, true);
17887      } else {
17888        // Any other iframe - still try to intercept
17889        setupIframeIntercept(iframe, false);
17890      }
17891    });
17892    
17893    console.log(`Attempted to intercept links in ${iframes.length} iframes`);
17894  }
17895  
17896  // Setup link interception for a specific iframe
17897  function setupIframeIntercept(iframe, isPrimary) {
17898    try {
17899      // Skip if iframe is not fully loaded yet
17900      if (!iframe.contentWindow || !iframe.contentDocument) {
17901        // Add load event to try again when loaded
17902        iframe.addEventListener('load', function() {
17903          setupIframeIntercept(iframe, isPrimary);
17904        });
17905        return;
17906      }
17907      
17908      // Try to access the iframe content - might fail due to same-origin policy
17909      try {
17910        const iframeDoc = iframe.contentDocument;
17911        const iframeWin = iframe.contentWindow;
17912        
17913        // Add event listener for all link clicks in the iframe
17914        iframeDoc.addEventListener('click', function(e) {
17915          const link = e.target.closest('a');
17916          if (!link || !link.href) return;
17917          
17918          // Skip special cases
17919          if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17920          if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17921          
17922          try {
17923            const url = new URL(link.href);
17924            
17925            // Check if same domain or relative URL
17926            const isSameDomain = url.hostname === window.location.hostname || 
17927                                url.hostname === iframe.src.hostname || 
17928                                !url.hostname;
17929            
17930            if (isSameDomain) {
17931              const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17932              const siteGuid = getSiteGuidFromPath();
17933              
17934              // First try to find a matching gallery/page
17935              const matchingItem = parent.findGalleryByPath ? 
17936                parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 
17937                findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17938              
17939              if (matchingItem) {
17940                // We found a match - prevent default and navigate
17941                e.preventDefault();
17942                e.stopPropagation();
17943                
17944                console.log('Iframe: Found matching content for link:', matchingItem.title);
17945                
17946                // Access parent window functions
17947                if (matchingItem.isPage && parent.loadPage) {
17948                  parent.loadPage(matchingItem.id);
17949                } else if (parent.loadGallery) {
17950                  parent.loadGallery(matchingItem.id);
17951                }
17952                
17953                // Update URL in parent window
17954                let targetUrl = url.href;
17955                if (isPreviewMode && siteGuid) {
17956                  // Ensure GUID is preserved in URL
17957                  const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17958                    url.pathname : 
17959                    `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17960                  
17961                  const newUrl = new URL(window.location.origin + pathname);
17962                  targetUrl = newUrl.href;
17963                }
17964                
17965                // Update browser URL
17966                parent.history.pushState(
17967                  { galleryId: matchingItem.id },
17968                  matchingItem.title || document.title,
17969                  targetUrl
17970                );
17971                
17972                // Update parent window title
17973                parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent);
17974                
17975                // Scroll parent to top
17976                parent.scrollTo(0, 0);
17977                return;
17978              } 
17979              // Special handling for preview mode - critical fix here!
17980              else if (isPreviewMode && siteGuid) {
17981                // Special handling for preview URLs
17982                e.preventDefault();
17983                e.stopPropagation();
17984                
17985                console.log('Iframe: No direct match but in preview mode - handling navigation');
17986                
17987                // Construct a URL that preserves the GUID
17988                let newPathname = url.pathname;
17989                if (!newPathname.startsWith(`/${siteGuid}/`)) {
17990                  newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17991                }
17992                
17993                // Create a URL for the browser history
17994                const browserUrl = new URL(window.location.origin);
17995                browserUrl.pathname = newPathname;
17996                
17997                console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href);
17998                
17999                // Update browser URL in parent
18000                parent.history.pushState(
18001                  { customPath: newPathname },
18002                  document.title,
18003                  browserUrl.href
18004                );
18005                
18006                // Handle the navigation in the parent window
18007                if (isPrimary && parent.document.getElementById('galleryFrame')) {
18008                  const galleryFrame = parent.document.getElementById('galleryFrame');
18009                  
18010                  // Create clean path for the gallery system
18011                  const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
18012                  
18013                  // Create wrapper URL for gallery content
18014                  const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
18015                    `?siteId=${siteGuid}` +
18016                    `&path=${encodeURIComponent(cleanPath)}` + 
18017                    `&directPath=true` +
18018                    `&t=${Date.now()}`; // Cache buster
18019                  
18020                  console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl);
18021                  galleryFrame.src = wrapperUrl;
18022                  
18023                  // Clear any active gallery ID since we're navigating directly
18024                  if (parent.activeGalleryId) {
18025                    console.log('Iframe: Clearing active gallery ID during direct navigation');
18026                    parent.activeGalleryId = null;
18027                  }
18028                } else {
18029                  // Fallback - navigate normally in case we can't handle it
18030                  console.log('Iframe: Gallery frame not found or not primary, navigating normally');
18031                  window.location.href = browserUrl.href;
18032                }
18033                return;
18034              }
18035            }
18036          } catch (iframeError) {
18037            console.error('Error handling iframe link:', iframeError);
18038          }
18039        });
18040        
18041        if (isPrimary) {
18042          console.log('Successfully intercepted links in primary gallery iframe');
18043        }
18044      } catch (sameOriginError) {
18045        // This is expected for cross-origin iframes
18046        if (isPrimary) {
18047          console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.');
18048          
18049          // Apply a fallback method - add postMessage listener
18050          setupPostMessageListener(iframe);
18051        }
18052      }
18053    } catch (error) {
18054      console.warn('Error setting up iframe link interception:', error);
18055    }
18056  }
18057  
18058  // Fallback: use postMessage for cross-origin iframes
18059  function setupPostMessageListener(iframe) {
18060    // Listen for messages from the iframe
18061    window.addEventListener('message', function(event) {
18062      try {
18063        // Verify the message is from our iframe
18064        if (event.source !== iframe.contentWindow) return;
18065        
18066        const data = event.data;
18067        
18068        // Check if this is a link click event
18069        if (data && data.type === 'linkClick' && data.href) {
18070          console.log('Received link click message from iframe:', data.href);
18071          
18072          // Create a URL object to work with the link
18073          const url = new URL(data.href);
18074          
18075          // Check if it's a local navigation
18076          if (url.hostname === window.location.hostname) {
18077            const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
18078            const siteGuid = getSiteGuidFromPath();
18079            
18080            // Try to find a matching gallery
18081            const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
18082            
18083            if (matchingItem) {
18084              console.log('Found matching gallery for postMessage link:', matchingItem.title);
18085              
18086              // Navigate using the appropriate function
18087              if (matchingItem.isPage && window.loadPage) {
18088                window.loadPage(matchingItem.id);
18089              } else if (window.loadGallery) {
18090                window.loadGallery(matchingItem.id);
18091              }
18092              
18093              // Update URL without reload
18094              let targetUrl = url.href;
18095              if (isPreviewMode && siteGuid) {
18096                // Ensure GUID is preserved
18097                const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
18098                  url.pathname : 
18099                  `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
18100                
18101                const newUrl = new URL(window.location.origin + pathname);
18102                targetUrl = newUrl.href;
18103              }
18104              
18105              window.history.pushState(
18106                { galleryId: matchingItem.id },
18107                matchingItem.title || document.title,
18108                targetUrl
18109              );
18110              
18111              // Update page title
18112              document.title = formatGalleryDocumentTitle(matchingItem.title);
18113              
18114              return;
18115            } 
18116            // Special handling for preview mode - Fixed!
18117            else if (isPreviewMode && siteGuid) {
18118              console.log('No direct match but in preview mode - handling navigation via postMessage');
18119              
18120              // Construct URL that preserves the GUID
18121              let newPathname = url.pathname;
18122              if (!newPathname.startsWith(`/${siteGuid}/`)) {
18123                newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
18124              }
18125              
18126              // Create browser URL
18127              const browserUrl = new URL(window.location.origin);
18128              browserUrl.pathname = newPathname;
18129              
18130              // Update browser URL 
18131              window.history.pushState(
18132                { customPath: newPathname },
18133                document.title,
18134                browserUrl.href
18135              );
18136              
18137              // Get the main gallery frame - NOT the iframe that sent the message
18138              const galleryFrame = document.getElementById('galleryFrame');
18139              if (galleryFrame) {
18140                // Create clean path without GUID
18141                const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
18142                
18143                // Create wrapper URL
18144                const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
18145                  `?siteId=${siteGuid}` +
18146                  `&path=${encodeURIComponent(cleanPath)}` + 
18147                  `&directPath=true` +
18148                  `&t=${Date.now()}`; // Cache buster
18149                
18150                console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl);
18151                galleryFrame.src = wrapperUrl;
18152                
18153                // Clear any active gallery ID
18154                if (window.activeGalleryId) {
18155                  window.activeGalleryId = null;
18156                }
18157                
18158                return;
18159              } else {
18160                console.warn('Gallery frame not found - cannot load content via postMessage');
18161              }
18162            }
18163          }
18164        }
18165      } catch (error) {
18166        console.error('Error processing postMessage from iframe:', error);
18167      }
18168    });
18169    
18170    // Inject a script into the iframe if possible to send messages back
18171    try {
18172      // We might be able to inject a script tag even in cross-origin scenarios
18173      // if the iframe is not yet loaded or using document.write
18174      iframe.addEventListener('load', function() {
18175        try {
18176          const script = iframe.contentDocument.createElement('script');
18177          script.textContent = `
18178            // Link interception script injected from parent
18179            document.addEventListener('click', function(e) {
18180              const link = e.target.closest('a');
18181              if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
18182                // Send message to parent window
18183                window.parent.postMessage({
18184                  type: 'linkClick',
18185                  href: link.href,
18186                  pathname: link.pathname
18187                }, '*');
18188                
18189                // Prevent default navigation
18190                e.preventDefault();
18191              }
18192            });
18193            console.log('Gallery link interceptor injected from parent');
18194          `;
18195          iframe.contentDocument.head.appendChild(script);
18196        } catch (e) {
18197          // This will likely fail for cross-origin iframes
18198          console.log('Could not inject script into iframe - expected for cross-origin frames');
18199        }
18200      });
18201    } catch (error) {
18202      console.warn('Error trying to inject script into iframe:', error);
18203    }
18204  }
18205  
18206  // Observer for new content/iframes
18207  function setupMutationObserver() {
18208    if (!window.MutationObserver) return;
18209    
18210    const observer = new MutationObserver(function(mutations) {
18211      let newIframesFound = false;
18212      
18213      mutations.forEach(function(mutation) {
18214        if (mutation.type === 'childList' && mutation.addedNodes.length) {
18215          // Check if any new iframes were added
18216          mutation.addedNodes.forEach(function(node) {
18217            // Check if the node is an iframe or contains iframes
18218            if (node.nodeName === 'IFRAME') {
18219              newIframesFound = true;
18220              setupIframeIntercept(node, node.id === 'galleryFrame');
18221            } else if (node.querySelectorAll) {
18222              const iframes = node.querySelectorAll('iframe');
18223              if (iframes.length > 0) {
18224                newIframesFound = true;
18225                iframes.forEach(function(iframe) {
18226                  setupIframeIntercept(iframe, iframe.id === 'galleryFrame');
18227                });
18228              }
18229            }
18230          });
18231        }
18232      });
18233      
18234      // If new iframes were found, update our cache
18235      if (newIframesFound) {
18236        refreshGalleryCache();
18237      }
18238    });
18239    
18240    // Observe the entire document
18241    observer.observe(document.body, {
18242      childList: true,
18243      subtree: true
18244    });
18245  }
18246  
18247  // Helper to check if a URL is a resource rather than a page
18248  function isResourceUrl(path) {
18249    const resourceExtensions = [
18250      '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 
18251      '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2'
18252    ];
18253    return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext));
18254  }
18255  
18256  // Get the site GUID from the path in preview mode
18257  function getSiteGuidFromPath() {
18258    if (window.location.hostname === 'preview.neonsky.app') {
18259      const pathParts = window.location.pathname.split('/').filter(
18259Boolean);
18260      if (pathParts.length > 0) {
18261        return pathParts[0];
18262      }
18263    }
18264    return null;
18265  }
18266  
18267
18268/**
18269 * Enhanced gallery path matching with improved accuracy:
18270 * 
18271 * This function finds the appropriate gallery based on URL path with better
18272 * prioritization and more precise matching criteria.
18273 * 
18274 * @param {string} path - The URL path to match
18275 * @param {boolean} isPreviewMode - Whether we're in preview mode
18276 * @param {string} siteGuid - The site GUID (for preview mode)
18277 * @returns {Object|null} The matching gallery or null if none found
18278 */
18279function findGalleryByPath(path, isPreviewMode, siteGuid) {
18280  // Ensure path is a string
18281  if (typeof path !== 'string') {
18282    console.warn('Invalid path provided to findGalleryByPath:', path);
18283    return null;
18284  }
18285
18286  // Determine which galleries array to use - check global variable first
18287  let galleriesData;
18288  let galleryCount = 0;
18289  
18290  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
18291    galleriesData = galleries;
18292    galleryCount = galleries.length;
18293  } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
18294    galleriesData = window.galleries;
18295    galleryCount = window.galleries.length;
18296  } else if (window.GALLERY_DATA_CACHE && 
18297             window.GALLERY_DATA_CACHE.initialized && 
18298             Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
18299    galleriesData = window.GALLERY_DATA_CACHE.galleries;
18300    galleryCount = window.GALLERY_DATA_CACHE.galleries.length;
18301  } else {
18302    console.warn('No galleries array available');
18303    return null;
18304  }
18305  
18306  console.log(`Searching for match among ${galleryCount} galleries`);
18307
18308  // Strip leading/trailing slashes without regex — template-literal embedding mangles / escapes
18309  // (e.g. /^/|/$/g becomes /^/|/$/g → 0), which left "/" unnormalized and broke home matching.
18310  function stripEdgeSlashes(value) {
18311    return String(value || '').split('/').filter(Boolean).join('/');
18312  }
18313  
18314  // Normalize the path for comparison (handle trailing/leading slashes)
18315  let normalizedPath = stripEdgeSlashes(path).toLowerCase();
18316  const originalNormalizedPath = normalizedPath; // Keep original for logging
18317  
18318  // If this is a preview URL, we need to handle the path differently
18319  if (isPreviewMode && siteGuid) {
18320    // Check if the path starts with the GUID
18321    if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) {
18322      // Remove the GUID prefix
18323      normalizedPath = normalizedPath.substring(siteGuid.length + 1);
18324    } else if (normalizedPath === siteGuid.toLowerCase()) {
18325      // This is just the GUID with no additional path
18326      normalizedPath = '';
18327    }
18328  }
18329  
18330  // Create a dash-normalized version (all sequences of dashes become single dashes)
18331  // This handles both double-dash patterns and any other dash inconsistencies
18332  const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-');
18333  
18334  console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`);
18335  
18336  // For empty paths, try to find the homepage
18337  if (!normalizedPath) {
18338    // Look for a gallery marked as home page
18339    const homePage = galleriesData.find(g => g.isHomePage === true);
18340    if (homePage) {
18341      console.log('Empty path - returning homepage:', homePage.title);
18342      return homePage;
18343    }
18344  }
18345
18346  // Array to store potential matches with their confidence levels
18347  const potentialMatches = [];
18348  
18349  // Process each gallery to find potential matches with confidence levels
18350  galleriesData.forEach(gallery => {
18351    // Skip invalid galleries
18352    if (!gallery || !gallery.id) return;
18353    
18354    // Store matching details for debugging
18355    const matchInfo = {
18356      gallery,
18357      confidence: 0,
18358      matchType: null,
18359      usedNormalizedDashes: false
18360    };
18361    
18362    // 1. EXACT SLUG MATCH (highest confidence)
18363    if (gallery.slug) {
18364      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18365      
18366      // Try original path
18367      if (gallerySlug === normalizedPath) {
18368        matchInfo.confidence = 100; // Max confidence
18369        matchInfo.matchType = 'exact_slug';
18370        potentialMatches.push({...matchInfo});
18371        return; // Continue to next gallery
18372      }
18373      
18374      // Try dash-normalized path
18375      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18376      if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) {
18377        matchInfo.confidence = 95; // Very high confidence
18378        matchInfo.matchType = 'exact_slug_normalized_dashes';
18379        matchInfo.usedNormalizedDashes = true;
18380        potentialMatches.push({...matchInfo});
18381        return; // Continue to next gallery
18382      }
18383    }
18384    
18385    // 2. EXACT URL PATH MATCH (very high confidence)
18386    if (gallery.url) {
18387      let galleryPath = gallery.url.toLowerCase();
18388      
18389      if (galleryPath.startsWith('http')) {
18390        try {
18391          const urlObj = new URL(galleryPath);
18392          galleryPath = urlObj.pathname;
18393        } catch (e) {
18394          // If URL parsing fails, use the string as is
18395        }
18396      }
18397      
18398      galleryPath = stripEdgeSlashes(galleryPath);
18399      
18400      // Try original path
18401      if (galleryPath === normalizedPath) {
18402        matchInfo.confidence = 90; // Very high confidence
18403        matchInfo.matchType = 'exact_url_path';
18404        potentialMatches.push({...matchInfo});
18405        return; // Continue to next gallery
18406      }
18407      
18408      // Try dash-normalized path
18409      const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-');
18410      if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) {
18411        matchInfo.confidence = 85; // High confidence
18412        matchInfo.matchType = 'exact_url_path_normalized_dashes';
18413        matchInfo.usedNormalizedDashes = true;
18414        potentialMatches.push({...matchInfo});
18415        return; // Continue to next gallery
18416      }
18417    }
18418    
18419    // 3. TITLE SLUG MATCH (high confidence)
18420    if (gallery.title) {
18421      const titleSlug = gallery.title.toLowerCase()
18422        .replace(/s+/g, '-')
18423        .replace(/[^w-]+/g, '')
18424        .replace(/--+/g, '-')
18425        .trim();
18426      
18427      // Try original path
18428      if (titleSlug === normalizedPath) {
18429        matchInfo.confidence = 80; // High confidence
18430        matchInfo.matchType = 'title_slug_match';
18431        potentialMatches.push({...matchInfo});
18432        return; // Continue to next gallery
18433      }
18434      
18435      // Try dash-normalized path
18436      const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-');
18437      if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) {
18438        matchInfo.confidence = 75; // Good confidence
18439        matchInfo.matchType = 'title_slug_match_normalized_dashes';
18440        matchInfo.usedNormalizedDashes = true;
18441        potentialMatches.push({...matchInfo});
18442        return; // Continue to next gallery
18443      }
18444    }
18445    
18446    // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA
18447    if (gallery.slug && normalizedPath.length > 5) {
18448      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18449      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18450      
18451      // Only if paths are substantial (to avoid false positives)
18452      if (normalizedPath.length >= 5 && gallerySlug.length >= 5) {
18453        // Try matching significant portions with dash normalization
18454        
18455        // If the normalized paths contain each other with substantial overlap
18456        if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 
18457            gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) {
18458          
18459          // Calculate overlap ratio based on the shorter string
18460          const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 
18461                               Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length);
18462          
18463          // Only consider matches with substantial overlap (over 70%)
18464          if (overlapRatio >= 0.7) {
18465            const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap
18466            
18467            matchInfo.confidence = score;
18468            matchInfo.matchType = 'partial_match_normalized';
18469            matchInfo.usedNormalizedDashes = true;
18470            matchInfo.overlapRatio = overlapRatio;
18471            potentialMatches.push({...matchInfo});
18472          }
18473        }
18474      }
18475    }
18476  });
18477  
18478  // No potential matches found
18479  if (potentialMatches.length === 0) {
18480    console.log('No gallery match found for path:', path);
18481    return null;
18482  }
18483  
18484  // Sort potential matches by confidence (highest first)
18485  potentialMatches.sort((a, b) => b.confidence - a.confidence);
18486  
18487  // Get the best match
18488  const bestMatch = potentialMatches[0];
18489  
18490  // Log detailed debugging info about the match
18491  console.log(`Best match for "${normalizedPath}":`, {
18492    title: bestMatch.gallery.title,
18493    id: bestMatch.gallery.id,
18494    matchType: bestMatch.matchType,
18495    confidence: bestMatch.confidence,
18496    usedNormalizedDashes: bestMatch.usedNormalizedDashes,
18497    totalMatches: potentialMatches.length
18498  });
18499  
18500  // If we have multiple close matches, log them for debugging
18501  if (potentialMatches.length > 1 && 
18502      potentialMatches[1].confidence >= bestMatch.confidence - 10) {
18503    console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({
18504      title: m.gallery.title,
18505      id: m.gallery.id,
18506      matchType: m.matchType,
18507      confidence: m.confidence
18508    }
18508)));
18509  }
18510  
18511  // Return the best matching gallery
18512  return bestMatch.gallery;
18513}
18514  
18515  // Make helpful functions available globally
18516  window.findGalleryByPath = findGalleryByPath;
18517  window.findGalleryById = findGalleryById;
18518  window.refreshGalleryCache = refreshGalleryCache;
18519  window.getSiteGuidFromPath = getSiteGuidFromPath;
18520  if (typeof getSiteGalleriesArray === 'function') {
18521    window.getSiteGalleriesArray = getSiteGalleriesArray;
18522  }
18523})();
18524
18525
18526
18527    </script>
18527
18528  </div>
18529  
18530<!-- Sidebar element form for adding new elements -->
18531<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet>
18532<p class="pe-edit-shell__title">Add sidebar element</p>
18533<p class="pe-hint">Select the type of element to add:</p>
18534
18535<div class="radio-group">
18536  <label>
18537    <input type="radio" name="sidebarElementType" value="text" checked> Text Block
18538  </label>
18539  <label>
18540    <input type="radio" name="sidebarElementType" value="image"> Image
18541  </label>
18542  <label>
18543    <input type="radio" name="sidebarElementType" value="social"> Social Icons
18544  </label>
18545  <label>
18546    <input type="radio" name="sidebarElementType" value="spacer"> Spacer
18547  </label>
18548</div>
18549
18550<div class="pe-footer-actions">
18551<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button>
18552<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button>
18553</div>
18554</div>
18555
18556 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet>
18557  <p class="pe-edit-shell__title">Add to menu</p>
18558  <input type="text" id="galleryTitle" placeholder="Gallery Title">
18559 <div class="radio-group">
18560   <label>
18561    <input type="radio" name="galleryType" value="integrated" checked> Gallery
18562  </label>
18563    <label>
18564    <input type="radio" name="galleryType" value="page"> Page
18565  </label>
18566  <label>
18567    <input type="radio" name="galleryType" value="folder"> Folder
18568  </label>
18569  <label>
18570    <input type="radio" name="galleryType" value="spacer"> Spacer
18571  </label>
18572    <label>
18573    <input type="radio" name="galleryType" value="external"> External URL
18574  </label>
18575</div>
18576  <div id="urlInputContainer">
18577    <input type="text" id="galleryUrl" placeholder="Gallery URL">
18578  </div>
18579  <div id="galleryParentContainer" style="display: none;" aria-hidden="true">
18580    <label for="galleryParent">Parent Menu:</label>
18581    <select id="galleryParent">
18582      <option value="">None (Top Level)</option>
18583    </select>
18584  </div>
18585  <div class="pe-footer-actions">
18586    <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button>
18587  </div>
18588    
18588<script>
18589
18590
18591      // Add event listeners to the radio buttons
18592      document.addEventListener('DOMContentLoaded', () => {
18593        const radioButtons = document.querySelectorAll('input[name="galleryType"]');
18594        radioButtons.forEach(radio => {
18595          radio.addEventListener('change', function() {
18596            const urlContainer = document.getElementById('urlInputContainer');
18597            if (this.value === 'external') {
18598              urlContainer.style.display = 'block';
18599            } else {
18600              urlContainer.style.display = 'none';
18601            }
18602          });
18603        });
18604      });
18605    </script>
18605
18606  </div>
18607
18608<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet>
18609  <h3 class="pe-edit-shell__title">Site Metadata</h3>
18610  
18611  <div class="form-group">
18612    <label for="metaTitle">Page Title (Search Results & Browsers)</label>
18613    <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results">
18614    <small>Recommended length: 50-60 characters</small>
18615  </div>
18616  
18617  <div class="form-group">
18618    <label for="metaDescription">Meta Description</label>
18619    <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea>
18620    <small>Recommended length: 150-160 characters</small>
18621  </div>
18622
18623  <div class="form-group">
18624    <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label>
18625    <input type="hidden" id="ogImageUrl" class="form-control" value="">
18626    <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn">
18627      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18628        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18629        <polyline points="17 8 12 3 7 8"></polyline>
18630        <line x1="12" y1="3" x2="12" y2="15"></line>
18631      </svg>
18632      <span id="uploadOgImageBtnText">Upload image</span>
18633    </button>
18634    <div id="ogImagePreviewContainer"></div>
18635    <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>
18636  </div>
18637  
18638  <div class="form-group">
18639    <label for="googleAnalytics">Google Analytics Code</label>
18640    <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX">
18641    <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small>
18642  </div>
18643  
18644  <div class="form-group">
18645    <label for="contentLanguage">Content Language</label>
18646    <select id="contentLanguage" class="form-control">
18647      <option value="en">English (en)</option>
18648      <option value="es">Spanish / Español (es)</option>
18649      <option value="fr">French / Français (fr)</option>
18650      <option value="ne">Nepali / नेपाली (ne)</option>
18651      <option value="de">German / Deutsch (de)</option>
18652      <option value="it">Italian / Italiano (it)</option>
18653      <option value="pt">Portuguese / Português (pt)</option>
18654      <option value="ru">Russian / Русский (ru)</option>
18655      <option value="zh">Chinese / 中文 (zh)</option>
18656      <option value="ja">Japanese / 日本語 (ja)</option>
18657      <option value="ko">Korean / 한국어 (ko)</option>
18658      <option value="ar">Arabic / العربية (ar)</option>
18659      <option value="hi">Hindi / हिन्दी (hi)</option>
18660    </select>
18661    <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small>
18662  </div>
18663  
18664  <div class="form-group">
18665    <label>
18666      <input type="checkbox" id="noIndexToggle"> 
18667      Hide site from search engines (noindex)
18668    </label>
18669    <small>Use this if the site is under development or shouldn't appear in search results</small>
18670  </div>
18671
18672  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18673    <h4 style="margin-bottom: 8px;">Schema / Identity</h4>
18674    <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>
18675    <label style="display: block; margin-bottom: 6px;">This site represents:</label>
18676    <div style="display: flex; gap: 20px; margin-bottom: 4px;">
18677      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18678        <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual
18679      </label>
18680      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18681        <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business
18682      </label>
18683    </div>
18684    <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small>
18685  </div>
18686
18687  <div class="form-group" id="schemaJobTitleGroup" style="display: none;">
18688    <label for="schemaJobTitle">Job Title</label>
18689    <input type="text" id="schemaJobTitle" class="form-control" placeholder="">
18690    <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small>
18691  </div>
18692
18693  <div class="form-group" id="schemaCityGroup" style="display: none;">
18694    <label for="schemaCity">City / Location</label>
18695    <input type="text" id="schemaCity" class="form-control" placeholder="">
18696    <small>Improves visibility in location-based searches.</small>
18697  </div>
18698
18699  <div class="form-group" id="schemaEmailGroup" style="display: none;">
18700    <label for="schemaEmail">Contact Email (for structured data)</label>
18701    <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]">
18702    <small>Embedded in structured data only — not displayed on the page.</small>
18703  </div>
18704
18705  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18706    <h4 style="margin-bottom: 8px;">
18706Image licensing (optional)</h4>
18707    <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>
18708    <label for="imageLicenseUrl">License page URL</label>
18709    <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing">
18710    <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small>
18711  </div>
18712
18713  <div class="form-group">
18714    <label for="acquireLicensePageUrl">Acquire license page URL</label>
18715    <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact">
18716    <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small>
18717  </div>
18718
18719  <div class="form-group">
18720    <label>
18721      <input type="checkbox" id="metadataCopyrightEnabled">
18722      Enable sidebar copyright footer
18723    </label>
18724    <small>When enabled, the copyright text shows at the bottom of the sidebar.</small>
18725  </div>
18726
18727  <div class="form-group" id="metadataCopyrightTextGroup">
18728    <label for="metadataCopyrightText">Copyright Text</label>
18729    <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name">
18730    <small>This text appears in the sidebar footer and inside the copyright overlay.</small>
18731  </div>
18732  
18733  <div class="form-group">
18734    <label for="faviconUrl">Favicon</label>
18735    <input type="hidden" id="faviconUrl" class="form-control">
18736    <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn">
18737      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18738        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18739        <polyline points="17 8 12 3 7 8"></polyline>
18740        <line x1="12" y1="3" x2="12" y2="15"></line>
18741      </svg>
18742      <span id="uploadFaviconBtnText">Upload Favicon</span>
18743    </button>
18744    <div id="faviconPreviewContainer"></div>
18745    <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small>
18746  </div>
18747
18748  <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18749    <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4>
18750    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18751      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.
18752      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.
18753      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).
18754    </p>
18755    <label>
18756      <input type="checkbox" id="customPublicSiteEnabled" />
18757      Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>)
18758    </label>
18759    <input type="hidden" id="customPublicSiteHtmlR2Key" value="" />
18760    <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" />
18761    <div class="form-group" style="margin-top: 10px;">
18762      <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button>
18763      <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button>
18764    </div>
18765    <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p>
18766  </div>
18767
18768  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18769    <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4>
18770    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18771      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
18772      <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.
18773    </p>
18774    <label>
18775      <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" />
18776      Require HTTP Basic password for the entire site
18777    </label>
18778    <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;">
18779      <div class="form-group">
18780        <label for="siteWideBasicAuthUsername">Username</label>
18781        <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" />
18782      </div>
18783      <div class="form-group">
18784        <label for="siteWideBasicAuthPassword">Password</label>
18785        <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" />
18786        <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small>
18787      </div>
18788      <div class="form-group">
18789        <label for="siteWideBasicAuthRealm">Realm (optional)</label>
18790        <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" />
18791        <small>Label shown in the browser login dialog.</small>
18792      </div>
18793    </div>
18794  </div>
18795  
18796  <div class="form-actions pe-footer-actions">
18797    <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button>
18798    <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button>
18799  </div>
18800</div>
18801
18802<script>
18803var customPublicSiteOptionHeld = false;
18804var customPublicSiteOptionKeyBound = false;
18805
18806function isMetadataEditorOpen() {
18807  var editor = document.getElementById('metadataEditor');
18808  if (!editor) return false;
18809  if (window.currentOpenForm === 'metadataEditor') return true;
18810  return editor.style.display === 'block';
18811}
18812
18813function syncCustomPublicSiteBypassVisibility() {
18814  var group = document.getElementById('customPublicSiteBypassGroup');
18815  if (!group) return;
18816  var show = isMetadataEditorOpen() && customPublicSiteOptionHeld;
18817  group.hidden = !show;
18818  group.style.display = show ? 'block' : 'none';
18819}
18820
18821function bindCustomPublicSiteOptionKeyListeners() {
18822  if (customPublicSiteOptionKeyBound) return;
18823  customPublicSiteOptionKeyBound = true;
18824
18825  document.addEventListener('keydown', function(e) {
18826    if (!e.altKey) return;
18827    customPublicSiteOptionHeld = true;
18828    syncCustomPublicSiteBypassVisibility();
18829  }, true);
18830
18831  document.addEventListener('keyup', function(e) {
18832    var isOptionRelease =
18833      e.key === 'Alt' ||
18834      e.key === 'Option' ||
18835      e.code === 'AltLeft' ||
18836      e.code === 'AltRight';
18837    if (!isOptionRelease) return;
18838    customPublicSiteOptionHeld = false;
18839    syncCustomPublicSiteBypassVisibility();
18840  }, true);
18841}
18842
18843window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility;
18844window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners;
18845
18846function updateSiteWideBasicAuthFieldState() {
18847  const en = document.getElementById('siteWideBasicAuthEnabled');
18848  const fields = document.getElementById('siteWideBasicAuthFields');
18849  if (fields) fields.style.display = en && en.checked ? 'block' : 'none';
18850}
18851
18852function updateSchemaFields() {
18853  const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked;
18854  const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked;
18855  const anySelected = isPerson || isOrg;
18856  const jobTitleGroup = document.getElementById('schemaJobTitleGroup');
18857  const cityGroup = document.getElementById('schemaCityGroup');
18858  const emailGroup = document.getElementById('schemaEmailGroup');
18859  if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none';
18860  if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none';
18861  if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none';
18862}
18863
18864function uploadCustomPublicHtmlFile() {
18865  const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || '');
18866  if (!siteId) {
18867    alert('Site ID not found. Refresh the page and try again.');
18868    return;
18869  }
18870  const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null;
18871  if (!authToken) {
18872    alert('Please log in first (press + on the page).');
18873    return;
18874  }
18875  const input = document.createElement('input');
18876  input.type = 'file';
18877  input.accept = '.html,.htm,text/html';
18878  input.addEventListener('change', function() {
18879    const file = input.files && input.files[0];
18880    if (!file) return;
18881    const formData = new FormData();
18882    formData.append('file', file);
18883    formData.append('guid', siteId);
18884    formData.append('purpose', 'custom-public-site');
18885    formData.append('pageId', '_site');
18886    formData.append('elementId', 'custom-public-html');
18887    formData.append('folderPath', '');
18888    var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file';
18889    var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload;
18890    fetch(uploadUrl, {
18891      method: 'POST',
18892      headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' },
18893      body: formData
18894    }).then(function(r) {
18895      return r.text().then(function(text) {
18896        var ct = (r.headers.get('content-type') || '').toLowerCase();
18897        if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) {
18898          throw new Error(
18899            'Server returned HTML instead of JSON — POST /api/upload-file is not reaching the Hydra worker. ' +
18900            'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.'
18901          );
18902        }
18903        if (r.status === 403 && ct.indexOf('application/json') === -1) {
18904          throw new Error(
18905            '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.'
18906          );
18907        }
18908        try {
18909          return JSON.parse(text);
18910        } catch (parseErr) {
18911          throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : ''));
18912        }
18913      });
18914    }).then(function(res) {
18915      if (res.success && res.key) {
18916        var keyEl = document.getElementById('customPublicSiteHtmlR2Key');
18917        var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore');
18918        var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
18919        if (keyEl) keyEl.value = res.key;
18920        if (fnEl) fnEl.value = file.name;
18921        if (lbl) lbl.textContent = 'Current file: ' + file.name + ' — click Save Metadata below to apply.';
18922        window.siteMetadata = window.siteMetadata || {};
18923        window.siteMetadata.customPublicSiteHtmlR2Key = res.key;
18924        window.siteMetadata.customPublicSiteHtmlFilename = file.name;
18925      } else {
18926        alert(res.error || res.details || 'Upload failed');
18927      }
18928    }).catch(function(e) {
18929      alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error'));
18930    });
18931  });
18932  input.click();
18933}
18934
18935function clearCustomPublicHtmlFile() {
18936  if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return;
18937  var k = document.getElementById('customPublicSiteHtmlR2Key');
18938  var fn = document.getElementById('customPublicSiteHtmlFilenameStore');
18939  var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
18940  if (k) k.value = '';
18941  if (fn) fn.value = '';
18942  if (lbl) lbl.textContent = 'No HTML file uploaded yet.';
18943  if (window.siteMetadata) {
18944    window.siteMetadata.customPublicSiteHtmlR2Key = '';
18945    window.siteMetadata.customPublicSiteHtmlFilename = '';
18946  }
18947}
18948
18949// Handle favicon upload button click
18950document.addEventListener('DOMContentLoaded', function() {
18951  const uploadBtn = document.getElementById('uploadFaviconBtn');
18952  if (uploadBtn) {
18953    uploadBtn.addEventListener('click', function() {
18954      uploadFavicon();
18955    });
18956  }
18957  const uploadOgBtn = document.getElementById('uploadOgImageBtn');
18958  if (uploadOgBtn) {
18959    uploadOgBtn.addEventListener('click', function() {
18960      uploadSiteOgImage();
18961    });
18962  }
18963  var upHtml = document.getElementById('uploadCustomPublicHtmlBtn');
18964  if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile);
18965  var rmHtml = document.getElementById('removeCustomPublicHtmlBtn');
18966  if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile);
18967  bindCustomPublicSiteOptionKeyListeners();
18968  syncCustomPublicSiteBypassVisibility();
18969  
18970  // Handle content language dropdown change
18971  const contentLanguageSelect = document.getElementById('contentLanguage');
18972  if (contentLanguageSelect) {
18973    contentLanguageSelect.addEventListener('change', function() {
18974      console.warn('Content language changed to:', this.value);
18975      // Update global variable immediately
18976      window.siteMetadata.contentLanguage = this.value;
18977      console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage);
18978    });
18979  }
18980
18981  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
18982  if (copyrightToggle) {
18983    copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState);
18984  }
18985
18986  updateMetadataCopyrightFieldState();
18987});
18988
18989// Function to handle favicon upload using ImageUploader
18990function uploadFavicon() {
18991  console.log('Starting favicon upload...');
18992  
18993  // Check if ImageUploader is available
18994  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
18995    alert('Image uploader not available. Please enter a favicon URL manually.');
18996    return;
18997  }
18998  
18999  try {
19000    // Create options for the ImageUploader
19001    const options = {
19002      title: 'Upload Favicon',
19003      containerType: 'favicon',
19004      maxWidth: 64,  // Favicons are typically small
19005      maxHeight: 64,
19006      quality: 0.9
19007    };
19008    
19009    // Call the ImageUploader
19010    window.ImageUploader.open(options, function(imageResult, imageInfo) {
19011      console.log('Favicon upload callback received:', imageInfo);
19012      
19013      // Get the favicon URL input field
19014      let faviconInput = document.getElementById('faviconUrl');
19015      console.warn('=== UPLOAD CALLBACK DEBUG ===');
19016      console.warn('Favicon input element found:', !!faviconInput);
19017      console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A');
19018      console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A');
19019      
19020      // If element not found, wait a bit and try again (metadata editor might not be open yet)
19021      if (!faviconInput) {
19022        console.warn('Favicon input not found, waiting 100ms and trying again...');
19023        setTimeout(() => {
19024          faviconInput = document.getElementById('faviconUrl');
19025          console.warn('Retry - Favicon input element found:', !!faviconInput);
19026          if (faviconInput) {
19027            console.warn('Retry - Setting favicon input value to:', imageInfo.url);
19028            faviconInput.value = imageInfo.url;
19029            console.warn('Retry - Favicon input value after setting:', faviconInput.value);
19030          } else {
19031            console.error('Retry - Favicon input field still not found');
19032          }
19033        }, 100);
19034        return;
19035      }
19036      
19037      // Set the uploaded image URL
19038      if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) {
19039        // R2 storage - use the URL
19040        console.warn('Setting favicon input value to:', imageInfo.url);
19041        faviconInput.value = imageInfo.url;
19042        console.warn('Favicon input value after setting:', faviconInput.value);
19043        
19044        // Also store in global variable as backup
19045        window.uploadedFaviconUrl = imageInfo.url;
19046        console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl);
19047        
19048        console.log('Favicon URL set to:', imageInfo.url);
19049        
19050        // Update button text
19051        const btnText = document.getElementById('uploadFaviconBtnText');
19052        if (btnText) {
19053          btnText.textContent = 'Change Favicon';
19054        }
19055        
19056        // Show preview
19057        showFaviconPreview(imageInfo.url);
19058      } else if (imageResult) {
19059        // Legacy base64 storage
19060        faviconInput.value = imageResult;
19061        console.log('Favicon set to base64 data');
19062        
19063        // Update button text
19064        const btnText = document.getElementById('uploadFaviconBtnText');
19065        if (btnText) {
19066          btnText.textContent = 'Change Favicon';
19067        }
19068        
19069        // Show preview
19070        showFaviconPreview(imageResult);
19071      } else {
19072        console.error('No image data received from uploader');
19073        alert('Upload failed. Please try again.');
19074        return;
19075      }
19076      
19077      // Show success message
19078      console.log('Favicon uploaded successfully');
19079    });
19080    
19081  } catch (error) {
19082    console.error('Error opening favicon uploader:', error);
19083    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
19084  }
19085}
19086
19087// Function to show a preview of the uploaded favicon
19088function showFaviconPreview(imageUrl) {
19089  const container = document.getElementById('faviconPreviewContainer');
19090  if (!container) {
19091    console.error('Favicon preview container not found');
19092    return;
19093  }
19094  
19095  container.innerHTML = 
19096    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
19097      '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' +
19098      '<div style="flex: 1;">' +
19099        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' +
19100        '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' +
19101      '</div>' +
19102      '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
19103        'Remove' +
19104      '</button>' +
19105    '</div>';
19106}
19107
19108// Function to remove the favicon
19109function removeFavicon() {
19110  const faviconInput = document.getElementById('faviconUrl');
19111  if (faviconInput) {
19112    faviconInput.value = '';
19113  }
19114  
19115  const container = document.getElementById('faviconPreviewContainer');
19116  if (container) {
19117    container.innerHTML = '';
19118  }
19119  
19120  const btnText = document.getElementById('uploadFaviconBtnText');
19121  if (btnText) {
19122    btnText.textContent = 'Upload Favicon';
19123  }
19124  
19125  console.log('Favicon removed');
19126}
19127
19128function uploadSiteOgImage() {
19129  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
19130    alert('Image uploader not available. Refresh the page and try again.');
19131    return;
19132  }
19133  try {
19134    window.ImageUploader.open({
19135      title: 'Upload social preview image',
19136      containerType: 'page',
19137      quality: 0.92
19138    }, function(imageResult, imageInfo) {
19139      const inp = document.getElementById('ogImageUrl');
19140      const setUrl = function(url) {
19141        if (inp) inp.value = url;
19142        const btnText = document.getElementById('uploadOgImageBtnText');
19143        if (btnText) btnText.textContent = 'Change image';
19144        showOgImagePreview(url);
19145        window.siteMetadata = window.siteMetadata || {};
19146        window.siteMetadata.ogImageUrl = url;
19147      };
19148      if (imageInfo && imageInfo.url) {
19149        setUrl(imageInfo.url);
19150      } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) {
19151        setUrl(imageResult);
19152      } else {
19153        alert('Upload failed. Please try again.');
19154      }
19155    });
19156  } catch (error) {
19157    console.error('Error opening OG image uploader:', error);
19158    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
19159  }
19160}
19161
19162function showOgImagePreview(imageUrl) {
19163  const container = document.getElementById('ogImagePreviewContainer');
19164  if (!container) return;
19165  container.innerHTML =
19166    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
19167      '<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;">' +
19168      '<div style="flex: 1;">' +
19169        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' +
19170        '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' +
19171      '</div>' +
19172      '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
19173        'Remove' +
19174      '</button>' +
19175    '</div>';
19176}
19177
19178function removeOgImage() {
19179  const inp = document.getElementById('ogImageUrl');
19180  if (inp) inp.value = '';
19181  if (window.siteMetadata) window.siteMetadata.ogImageUrl = '';
19182  const container = document.getElementById('ogImagePreviewContainer');
19183  if (container) container.innerHTML = '';
19184  const btnText = document.getElementById('uploadOgImageBtnText');
19185  if (btnText) btnText.textContent = 'Upload image';
19186}
19187</script>
19187
19188
19189  <!-- Import Classic Form -->
19190<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet>
19191  <h3>Import Galleries from JSON or Classic GUID</h3>
19192  <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p>
19193  
19194  <div class="form-group">
19195    <label for="pastedJson">Pasted Site JSON:</label>
19196    <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea>
19197  </div>
19198
19199  <div class="import-separator">OR</div>
19200
19201  <div class="form-group">
19202    <label for="classicGuid">Classic GUID or Import ID:</label>
19203    <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)">
19204    <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small>
19205  </div>
19206
19207    <div class="form-group">
19208      <label>
19209        <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;">
19210        Replace existing menu structure
19211      </label>
19212      <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>
19213  </div>
19214
19215  <div class="form-group">
19216    <label for="importParent">Import under:</label>
19217    <select id="importParent">
19218      <option value="">Top Level (No Parent)</option>
19219    </select>
19220  </div>
19221
19222  <div class="form-group">
19223    <label>
19224      <input type="checkbox" id="createSubmenu" checked>
19225      Create submenu for all imported galleries
19226    </label>
19227  </div>
19228
19229  <div class="form-group" id="submenuTitleContainer">
19230    <label for="submenuTitle">Submenu title:</label>
19231    <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries">
19232  </div>
19233
19234  <div id="importStatus" class="import-status" style="display: none;">
19235    <div class="import-progress"><div class="progress-bar"></div></div>
19236    <div class="status-message">Processing...</div>
19237  </div>
19238
19239  <div class="form-actions">
19240    <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button>
19241    <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button>
19242    <button class="btn" onclick="toggleImportClassicForm()">Cancel</button>
19243  </div>
19244</div>
19245  <div class="tree-container">
19246    <div id="galleryTree" data-help-id="menu-tree"></div>
19247  </div>
19248
19249    <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js -->
19250  <div class="sidebar-elements-container"></div>
19251</div>
19252<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1">
19253  <iframe class="gallery-iframe" id="galleryFrame" title="Book | Eirini Vourloumis"></iframe>
19254</div>
19255</div>
19256
19257<!-- Cookie Notice (compliance mode — only when Display Cookie Notice is enabled) -->
19258
19259
19260<script>
19261// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized
19262(function() {
19263  let hasRevealed = false; // Ensure we only reveal once per page load
19264  
19265  function revealContent() {
19266    if (hasRevealed) return; // Prevent multiple calls
19267    hasRevealed = true;
19268    document.documentElement.classList.add('hydra-ready');
19269    document.body.classList.add('hydra-ready');
19270    console.log('✅ Content revealed - hydra-ready class added');
19271  }
19272  
19273  // Check if this page has a gallery container
19274  function hasGalleryContainer() {
19275    return !!document.getElementById('neon-gallery-container') || 
19276           !!document.querySelector('.gallery-wrapper') ||
19277           !!document.querySelector('.masonry-container');
19278  }
19279  
19280  // Check if gallery is already initialized
19281  function isGalleryInitialized() {
19282    return window.neonGalleryState?.initialized || 
19283           window.neonGalleryState?.domInitialized ||
19284           (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized());
19285  }
19286  
19287  // Main reveal logic
19288  function attemptReveal() {
19289    const hasGallery = hasGalleryContainer();
19290    const galleryInitialized = isGalleryInitialized();
19291    
19292    // If no gallery, reveal immediately after CSS loads
19293    if (!hasGallery) {
19294      if (document.readyState === 'complete') {
19295        revealContent();
19296      } else {
19297        window.addEventListener('load', revealContent, { once: true });
19298      }
19299      return;
19300    }
19301    
19302    // If gallery exists and is initialized, reveal
19303    if (hasGallery && galleryInitialized) {
19304      revealContent();
19305      return;
19306    }
19307    
19308    // If gallery exists but not initialized, wait for initialization
19309    if (hasGallery && !galleryInitialized) {
19310      console.log('⏳ Waiting for gallery initialization before revealing content...');
19311      
19312      // Listen for gallery-loaded event
19313      window.addEventListener('gallery-loaded', function() {
19314        console.log('✅ Gallery-loaded event received');
19315        setTimeout(revealContent, 100); // Small delay to ensure DOM is updated
19316      }, { once: true });
19317      
19318      // Listen for page-loaded event
19319      window.addEventListener('page-loaded', function() {
19320        console.log('✅ Page-loaded event received');
19321        setTimeout(revealContent, 100);
19322      }, { once: true });
19323      
19324      // Fallback: Poll for gallery initialization (max 10 seconds)
19325      let pollAttempts = 0;
19326      const maxPollAttempts = 100; // 10 seconds (100 * 100ms)
19327      const pollInterval = setInterval(function() {
19328        pollAttempts++;
19329        if (isGalleryInitialized()) {
19330          console.log('✅ Gallery initialized detected via polling');
19331          clearInterval(pollInterval);
19332          revealContent();
19333        } else if (pollAttempts >= maxPollAttempts) {
19334          console.warn('⚠️ Gallery initialization timeout - revealing content anyway');
19335          clearInterval(pollInterval);
19336          revealContent(); // Reveal anyway to prevent blank screen
19337        }
19338      }, 100);
19339      
19340      // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen)
19341      setTimeout(function() {
19342        if (!hasRevealed) {
19343          console.warn('⚠️ Safety timeout reached - revealing content to prevent blank screen');
19344          revealContent();
19345        }
19346      }, 8000);
19347    }
19348  }
19349  
19350  // Start the reveal process
19351  if (document.readyState === 'loading') {
19352    document.addEventListener('DOMContentLoaded', function() {
19353      // Wait a bit for scripts to initialize
19354      setTimeout(attemptReveal, 100);
19355    });
19356  } else if (document.readyState === 'interactive' || document.readyState === 'complete') {
19357    // DOM already ready, wait a bit for scripts
19358    setTimeout(attemptReveal, 100);
19359  } else {
19360    // Fallback
19361    window.addEventListener('load', attemptReveal, { once: true });
19362  }
19363})();
19364</script>
19364
19365
19366<script>
19367// ============================================
19368// LANGUAGE TRANSLATION SYSTEM
19369// ============================================
19370
19371(function() {
19372  // CRITICAL: Only initialize translation system if a languages element exists
19373  // This ensures sites without languages element have ZERO overhead
19374  const checkForLanguagesElement = function() {
19375    // Wait for SidebarManager to be initialized
19376    if (!window.SidebarManager || !window.SidebarManager.elements) {
19377      return false;
19378    }
19379    
19380    // Check if any languages element exists
19381    const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages');
19382    
19383    if (!hasLanguagesElement) {
19384      console.warn('No languages element found - translation system disabled');
19385      return false;
19386    }
19387    
19388    return true;
19389  };
19390  
19391  // Check for languages element, retry if SidebarManager not ready yet
19392  let initAttempts = 0;
19393  const maxAttempts = 10;
19394  
19395  function tryInitialize() {
19396    if (checkForLanguagesElement()) {
19397      console.warn('✅ Languages element found - initializing translation system');
19398      initializeTranslationSystem();
19399    } else {
19400      initAttempts++;
19401      if (initAttempts < maxAttempts) {
19402        // Retry in 500ms (total max wait: 5 seconds)
19403        setTimeout(tryInitialize, 500);
19404      } else {
19405        console.warn('Translation system not needed for this site - no languages element found after 5 seconds');
19406      }
19407    }
19408  }
19409  
19410  // Start checking after initial delay
19411  setTimeout(tryInitialize, 500);
19412  
19413  function initializeTranslationSystem() {
19414    console.warn('✅ Translation system initialized successfully');
19415    
19416    let currentLanguage = localStorage.getItem('hydra_language') || 'en';
19417    const originalTexts = new Map();
19418    
19419    // Expose test function for debugging
19420    window.testTranslation = function(langCode) {
19421      console.warn('🧪 Manual translation test triggered for:', langCode);
19422      console.warn('🔍 Dispatching test event...');
19423      window.dispatchEvent(new CustomEvent('hydra:language-change', {
19424        detail: { code: langCode || 'es', name: 'Test' }
19425      }));
19426      console.warn('✅ Test event dispatched - check if listener caught it above');
19427    };
19428    
19429    console.warn('✅ Event listener for "hydra:language-change" is now active');
19430    
19431    // Watch for edit mode changes and restore default language
19432    const editModeObserver = new MutationObserver(function(mutations) {
19433    mutations.forEach(function(mutation) {
19434      if (mutation.attributeName === 'class') {
19435        const isEditMode = document.body.classList.contains('edit-mode-active');
19436        if (isEditMode) {
19437          console.warn('Edit mode activated - restoring default language');
19438          // Restore original text
19439          restoreOriginalText();
19440          
19441          // Update language buttons to show default language
19442          const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19443          const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19444          
19445          document.querySelectorAll('.language-button-live').forEach(btn => {
19446            if (btn.getAttribute('data-lang-code') === defaultLang) {
19447              btn.style.background = 'var(--menu-active-color, #4682B4)';
19448            } else {
19449              btn.style.background = 'transparent';
19450            }
19451          });
19452        }
19453      }
19454    });
19455  });
19456  
19457  // Start observing body for class changes
19458  if (document.body) {
19459    editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
19460  }
19461  
19462  // Watch for dynamically loaded content (galleries, pages, etc.) and translate it
19463  const contentObserver = new MutationObserver(function(mutations) {
19464    // Only process if we're not in default language and not in edit mode
19465    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19466    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19467    const isEditMode = document.body.classList.contains('edit-mode-active');
19468    
19469    if (currentLanguage === defaultLang || isEditMode) {
19470      return; // Don't translate if in default language or edit mode
19471    }
19472    
19473    // Check if new text content was added
19474    let hasNewTextContent = false;
19475    mutations.forEach(function(mutation) {
19476      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
19477        mutation.addedNodes.forEach(function(node) {
19478          if (node.nodeType === Node.ELEMENT_NODE) {
19479            // Check if the added element contains text we should translate
19480            const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text');
19481            if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) {
19482              hasNewTextContent = true;
19483            }
19484          }
19485        });
19486      }
19487    });
19488    
19489    if (hasNewTextContent) {
19490      console.warn('New content detected - translating to:', currentLanguage);
19491      // Debounce to avoid translating multiple times rapidly
19492      clearTimeout(window._translationDebounce);
19493      window._translationDebounce = setTimeout(function() {
19494        translatePage(currentLanguage, defaultLang);
19495      }, 300);
19496    }
19497  });
19498  
19499  // Observe the gallery container and page content areas for new content
19500  const galleryContainer = document.querySelector('.gallery-container');
19501  if (galleryContainer) {
19502    contentObserver.observe(galleryContainer, { 
19503      childList: true, 
19504      subtree: true 
19505    });
19506  }
19507  
19508  // Also observe the main content area
19509  const mainContent = document.querySelector('.container');
19510  if (mainContent) {
19511    contentObserver.observe(mainContent, { 
19512      childList: true, 
19513      subtree: true 
19514    });
19515  }
19516  
19517  // Listen for language change events
19518  window.addEventListener('hydra:language-change', async function(e) {
19519    const { code, name } = e.detail;
19520    console.warn('🌐 Language changed to:', code, name);
19521    
19522    // CRITICAL: Ignore language changes in edit mode
19523    if (document.body.classList.contains('edit-mode-active')) {
19524      console.warn('⚠️ Edit mode active - ignoring language change');
19525      // Reset to default language
19526      const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19527      const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19528      
19529      // Update button states to show default language
19530      document.querySelectorAll('.language-button-live').forEach(btn => {
19531        if (btn.getAttribute('data-lang-code') === defaultLang) {
19532          btn.style.background = 'var(--menu-active-color, #4682B4)';
19533        } else {
19534          btn.style.background = 'transparent';
19535        }
19536      });
19537      
19538      // Show message to user
19539      console.warn('Language switching is disabled in edit mode');
19540      return;
19541    }
19542    
19543    // Update all language buttons
19544    document.querySelectorAll('.language-button-live').forEach(btn => {
19545      if (btn.getAttribute('data-lang-code') === code) {
19546        btn.style.background = 'var(--menu-active-color, #4682B4)';
19547      } else {
19548        btn.style.background = 'transparent';
19549      }
19550    });
19551    
19552    // Save preference
19553    localStorage.setItem('hydra_language', code);
19554    currentLanguage = code;
19555    
19556    // Get default language from languages element
19557    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19558    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19559    
19560    if (code === defaultLang) {
19561      // Restore original language
19562      restoreOriginalText();
19563    } else {
19564      // Translate page
19565      await translatePage(code, defaultLang);
19566    }
19567  });
19568  
19569  async function translatePage(targetLang, sourceLang) {
19570    console.warn(`🔄 Translating page from ${sourceLang} to ${targetLang}`);
19571    
19572    // Elements to translate
19573    const selectors = [
19574      '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode)
19575      '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode)
19576      'h1:not(.edit-mode-active h1)', 
19577      'h2:not(.edit-mode-active h2)', 
19578      'h3:not(.edit-mode-active h3)',
19579      'p:not(.edit-mode-active p)'
19580    ];
19581    
19582    const elements = document.querySelectorAll(selectors.join(', '));
19583    
19584    for (const element of elements) {
19585      // Skip if already translated to this language
19586      if (element.dataset.translatedTo === targetLang) continue;
19587      
19588      // Skip if element is empty
19589      const text = element.textContent.trim();
19590      if (!text || text.length === 0) continue;
19591      
19592      // Store original text
19593      if (!originalTexts.has(element)) {
19594        originalTexts.set(element, text);
19595      }
19596      
19597      // Check translation cache
19598      const cacheKey = `translate_${text}_${targetLang}`;
19599      let translated = getFromCache(cacheKey);
19600      
19601      if (translated) {
19602        element.textContent = translated;
19603        element.dataset.translatedTo = targetLang;
19604      } else {
19605        // Translate
19606        try {
19607          translated = await translate(text, sourceLang, targetLang);
19608          if (translated && translated !== text) {
19609            element.textContent = translated;
19610            element.dataset.translatedTo = targetLang;
19611            saveToCache(cacheKey, translated);
19612          }
19613        } catch (error) {
19614          console.error('Translation error:', error);
19615        }
19616      }
19617    }
19618  }
19619  
19620  function restoreOriginalText() {
19621    originalTexts.forEach((originalText, element) => {
19622      if (element && element.isConnected) {
19623        element.textContent = originalText;
19624        delete element.dataset.translatedTo;
19625      }
19626    });
19627  }
19628  
19629  async function translate(text, sourceLang, targetLang) {
19630    if (!text || text.trim().length === 0) return text;
19631    
19632    // Try Browser Translation API (Free, Chrome 130+)
19633    if ('translation' in self && 'createTranslator' in self.translation) {
19634      try {
19635        console.warn('🔄 Using Browser Translation API');
19636        const translator = await self.translation.createTranslator({
19637          sourceLanguage: sourceLang,
19638          targetLanguage: targetLang
19639        });
19640        const result = await translator.translate(text);
19641        console.warn('✅ Translation successful via Browser API');
19642        return result;
19643      } catch (error) {
19644        console.warn('❌ Browser translation failed:', error);
19645      }
19646    } else {
19647      console.warn('⚠️ Browser Translation API not available');
19648      console.warn('   To enable in Chrome: chrome://flags/#translation-api');
19649      console.warn('   OR add Google Translate API key to use paid translation');
19650    }
19651    
19652    // TODO: Add Google Translate API fallback here
19653    // For now, return original text
19654    console.warn('⚠️ No translation provider configured - returning original text');
19655    return text;
19656  }
19657  
19658  // Cache management
19659  function getFromCache(key) {
19660    try {
19661      const item = localStorage.getItem(`hydra_${key}`);
19662      if (!item) return null;
19663      
19664      const { value, expiry } = JSON.parse(item);
19665      if (expiry && Date.now() > expiry) {
19666        localStorage.removeItem(`hydra_${key}`);
19667        return null;
19668      }
19669      return value;
19670    } catch {
19671      return null;
19672    }
19673  }
19674  
19675  function saveToCache(key, value) {
19676    try {
19677      const item = {
19678        value,
19679        expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
19680      };
19681      localStorage.setItem(`hydra_${key}`, JSON.stringify(item));
19682    } catch (error) {
19683      console.warn('Cache save failed:', error);
19684    }
19685  }
19686  
19687  } // End of initializeTranslationSystem
19688})();
19689</script>
19689
19690
19691
19692
19693<script>
19694window.HYDRA_HELP_ENABLED = true;
19695function loadHelpSystemScript() {
19696  if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve();
19697  return new Promise(function(resolve, reject) {
19698    var existing = document.querySelector('script[data-hydra-help-system]');
19699    if (existing) {
19700      existing.addEventListener('load', function() { resolve(); });
19701      existing.addEventListener('error', reject);
19702      return;
19703    }
19704    var s = document.createElement('script');
19705    s.src = '/api/help-system.js?v=686c85345749';
19706    s.async = true;
19707    s.setAttribute('data-hydra-help-system', '1');
19708    s.onload = function() { resolve(); };
19709    s.onerror = reject;
19710    document.head.appendChild(s);
19711  });
19712}
19713function openHydraHelp(opts) {
19714  opts = opts || {};
19715  if (!window.HYDRA_HELP_ENABLED) return;
19716  var token = window.didToken || localStorage.getItem('hydra_auth_token');
19717  var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
19718  if (!token || !isAdmin) return;
19719  var p = loadHelpSystemScript();
19720  Promise.resolve(p).then(function() {
19721    if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init();
19722  }).then(function() {
19723    if (window.HelpSystem && window.HelpSystem.openHub) {
19724      window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined);
19725    }
19726  }).catch(function(err) {
19727    console.error('HelpSystem init failed:', err);
19728  });
19729}
19730window.openHydraHelp = openHydraHelp;
19731</script>
19731
19732
19733<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>
19733
19734</body>
19735</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.